sourcecode 1.3.0__tar.gz → 1.4.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-1.3.0 → sourcecode-1.4.0}/PKG-INFO +1 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/pyproject.toml +1 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/ast_extractor.py +123 -40
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/cli.py +44 -10
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/contract_model.py +7 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/contract_pipeline.py +8 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/dependency_analyzer.py +42 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/java.py +124 -5
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/project.py +33 -5
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/tooling.py +5 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/graph_analyzer.py +20 -67
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/schema.py +11 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/serializer.py +64 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_graph_analyzer_polyglot.py +4 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration.py +2 -1
- {sourcecode-1.3.0 → sourcecode-1.4.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/.gitignore +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/.ruff.toml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/LICENSE +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/README.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/SECURITY.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/docs/privacy.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/docs/schema.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/raw +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/run_cli.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/__init__.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/conftest.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_ast_extractor.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_block1_reliability.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_block2_coverage.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_block5_quality.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_cli.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_context_scorer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_contract_pipeline.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_dependency_analyzer_node_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detector_dotnet.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detector_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_hybrid_inference.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_dependencies.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_java_spring_integration.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_packaging.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_phase1_improvements.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_pipeline_integrity.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_real_projects.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_redactor.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_scanner.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_signal_hierarchy.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_summarizer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_telemetry.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.4.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -940,19 +940,51 @@ def _extract_python(path: str, source: str) -> FileContract:
|
|
|
940
940
|
|
|
941
941
|
|
|
942
942
|
# ---------------------------------------------------------------------------
|
|
943
|
-
#
|
|
943
|
+
# ---------------------------------------------------------------------------
|
|
944
|
+
# Enhanced Java extraction (regex-based, annotation-aware)
|
|
944
945
|
# ---------------------------------------------------------------------------
|
|
945
946
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
947
|
+
_JAVA_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([^;\s]+)\s*;', re.MULTILINE)
|
|
948
|
+
|
|
949
|
+
# Single annotation on one line (captures name + optional parens args)
|
|
950
|
+
_JAVA_ANNO_LINE_RE = re.compile(r'^\s*(@[\w.]+(?:\s*\([^)]*\))?)\s*$')
|
|
951
|
+
# Class/interface/enum declaration line (public or package-private)
|
|
952
|
+
_JAVA_CLASS_LINE_RE = re.compile(
|
|
953
|
+
r'(?:public\s+)?(?:(?:abstract|final|static|sealed)\s+)*'
|
|
954
|
+
r'(class|interface|enum|@interface)\s+(\w+)'
|
|
955
|
+
r'(?:\s+extends\s+([\w.]+))?'
|
|
956
|
+
r'(?:\s+implements\s+([\w.,\s<>]+?))?'
|
|
957
|
+
r'(?=\s*[\{<])'
|
|
950
958
|
)
|
|
951
|
-
|
|
952
|
-
|
|
959
|
+
# Public method: up to 12 leading spaces, return type, name, open paren
|
|
960
|
+
_JAVA_PUB_METHOD_LINE_RE = re.compile(
|
|
961
|
+
r'^\s{0,12}public\s+(?:(?:static|final|synchronized|abstract|default)\s+)*'
|
|
962
|
+
r'[\w<>\[\]?,\s]+?\s+(\w+)\s*\(',
|
|
953
963
|
re.MULTILINE,
|
|
954
964
|
)
|
|
955
|
-
|
|
965
|
+
# @Autowired or @Inject field
|
|
966
|
+
_JAVA_FIELD_DECL_RE = re.compile(
|
|
967
|
+
r'^\s*(?:private|protected|public)?\s*'
|
|
968
|
+
r'([\w<>.,\[\]? ]+?)\s+(\w+)\s*[;=]'
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _java_collect_preceding_annotations(lines: list[str], decl_idx: int) -> list[str]:
|
|
973
|
+
"""Walk back from decl_idx and collect @annotation lines immediately before it."""
|
|
974
|
+
annotations: list[str] = []
|
|
975
|
+
i = decl_idx - 1
|
|
976
|
+
while i >= 0:
|
|
977
|
+
stripped = lines[i].strip()
|
|
978
|
+
if not stripped or stripped.startswith("//") or stripped.startswith("*"):
|
|
979
|
+
i -= 1
|
|
980
|
+
continue
|
|
981
|
+
m = re.match(r'(@[\w.]+(?:\s*\([^)]*\))?)', stripped)
|
|
982
|
+
if m:
|
|
983
|
+
annotations.insert(0, m.group(1))
|
|
984
|
+
i -= 1
|
|
985
|
+
else:
|
|
986
|
+
break
|
|
987
|
+
return annotations
|
|
956
988
|
|
|
957
989
|
|
|
958
990
|
def _extract_java(path: str, source: str) -> FileContract:
|
|
@@ -960,35 +992,93 @@ def _extract_java(path: str, source: str) -> FileContract:
|
|
|
960
992
|
types: list[TypeDefinition] = []
|
|
961
993
|
functions: list[FunctionSignature] = []
|
|
962
994
|
imports: list[ImportRecord] = []
|
|
995
|
+
autowired_fields: list[dict] = []
|
|
996
|
+
|
|
997
|
+
lines = source.splitlines()
|
|
998
|
+
|
|
999
|
+
# Pass 1: collect imports
|
|
1000
|
+
seen_sources: set[str] = set()
|
|
1001
|
+
for m in _JAVA_IMPORT_RE.finditer(source):
|
|
1002
|
+
full_import = m.group(1).strip()
|
|
1003
|
+
if full_import not in seen_sources:
|
|
1004
|
+
seen_sources.add(full_import)
|
|
1005
|
+
imports.append(ImportRecord(source=full_import, kind="named", symbols=[]))
|
|
963
1006
|
|
|
964
|
-
#
|
|
965
|
-
|
|
966
|
-
name = m.group(2)
|
|
967
|
-
extends_str = m.group(3)
|
|
968
|
-
implements_str = m.group(4)
|
|
969
|
-
all_extends: list[str] = []
|
|
970
|
-
if extends_str:
|
|
971
|
-
all_extends.append(extends_str.strip())
|
|
972
|
-
if implements_str:
|
|
973
|
-
all_extends.extend(i.strip() for i in implements_str.split(",") if i.strip())
|
|
974
|
-
types.append(TypeDefinition(name=name, kind="class", fields=[], extends=all_extends))
|
|
975
|
-
exports.append(ExportRecord(name=name, kind="class"))
|
|
976
|
-
|
|
977
|
-
class_names = {t.name for t in types}
|
|
978
|
-
|
|
979
|
-
# Public method signatures (one-line heuristic)
|
|
1007
|
+
# Pass 2: line-by-line scan for classes, methods, @Autowired fields
|
|
1008
|
+
class_names: set[str] = set()
|
|
980
1009
|
seen_methods: set[str] = set()
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1010
|
+
autowired_pending = False
|
|
1011
|
+
|
|
1012
|
+
for idx, line in enumerate(lines):
|
|
1013
|
+
stripped = line.strip()
|
|
1014
|
+
|
|
1015
|
+
# Detect @Autowired / @Inject annotations
|
|
1016
|
+
if stripped.startswith("@Autowired") or stripped.startswith("@Inject"):
|
|
1017
|
+
autowired_pending = True
|
|
987
1018
|
continue
|
|
988
|
-
|
|
989
|
-
|
|
1019
|
+
|
|
1020
|
+
# Capture autowired field on next non-annotation, non-blank line
|
|
1021
|
+
if autowired_pending and stripped and not stripped.startswith("@"):
|
|
1022
|
+
autowired_pending = False
|
|
1023
|
+
fm = _JAVA_FIELD_DECL_RE.match(line)
|
|
1024
|
+
if fm:
|
|
1025
|
+
type_name = fm.group(1).strip().split()[-1] # last word = simple type
|
|
1026
|
+
field_name = fm.group(2).strip()
|
|
1027
|
+
if field_name and type_name and field_name not in {"class", "interface"}:
|
|
1028
|
+
autowired_fields.append({"type": type_name, "name": field_name})
|
|
1029
|
+
elif stripped and not stripped.startswith("@"):
|
|
1030
|
+
autowired_pending = False
|
|
1031
|
+
|
|
1032
|
+
# Class/interface/enum declaration
|
|
1033
|
+
cm = _JAVA_CLASS_LINE_RE.search(stripped)
|
|
1034
|
+
if cm and ("class " in stripped or "interface " in stripped or "enum " in stripped):
|
|
1035
|
+
kind_kw = cm.group(1) # class | interface | enum | @interface
|
|
1036
|
+
name = cm.group(2)
|
|
1037
|
+
extends_str = cm.group(3)
|
|
1038
|
+
implements_str = cm.group(4)
|
|
1039
|
+
|
|
1040
|
+
annotations = _java_collect_preceding_annotations(lines, idx)
|
|
1041
|
+
all_extends: list[str] = []
|
|
1042
|
+
if extends_str:
|
|
1043
|
+
all_extends.append(extends_str.strip())
|
|
1044
|
+
|
|
1045
|
+
implements_list: list[str] = []
|
|
1046
|
+
if implements_str:
|
|
1047
|
+
implements_list = [i.strip() for i in implements_str.split(",") if i.strip()]
|
|
1048
|
+
|
|
1049
|
+
export_kind = "class" if kind_kw in ("class", "@interface") else kind_kw
|
|
1050
|
+
exports.append(ExportRecord(
|
|
1051
|
+
name=name,
|
|
1052
|
+
kind=export_kind,
|
|
1053
|
+
annotations=annotations,
|
|
1054
|
+
extends=extends_str.strip() if extends_str else None,
|
|
1055
|
+
implements=implements_list,
|
|
1056
|
+
))
|
|
1057
|
+
types.append(TypeDefinition(
|
|
1058
|
+
name=name,
|
|
1059
|
+
kind=export_kind,
|
|
1060
|
+
fields=[],
|
|
1061
|
+
extends=all_extends,
|
|
1062
|
+
))
|
|
1063
|
+
class_names.add(name)
|
|
1064
|
+
|
|
1065
|
+
# Pass 3: public methods with their preceding annotations
|
|
1066
|
+
for m in _JAVA_PUB_METHOD_LINE_RE.finditer(source):
|
|
1067
|
+
sig_text = m.group(0).strip()
|
|
1068
|
+
mname = m.group(1)
|
|
1069
|
+
if (mname in class_names or mname in seen_methods
|
|
1070
|
+
or mname in {"if", "for", "while", "switch", "return", "new"}):
|
|
990
1071
|
continue
|
|
991
1072
|
seen_methods.add(mname)
|
|
1073
|
+
# Find line index for this match to collect preceding annotations
|
|
1074
|
+
line_start = source.count("\n", 0, m.start())
|
|
1075
|
+
annotations = _java_collect_preceding_annotations(lines, line_start)
|
|
1076
|
+
exports.append(ExportRecord(
|
|
1077
|
+
name=mname,
|
|
1078
|
+
kind="method",
|
|
1079
|
+
annotations=annotations,
|
|
1080
|
+
signature=sig_text,
|
|
1081
|
+
))
|
|
992
1082
|
functions.append(FunctionSignature(
|
|
993
1083
|
name=mname,
|
|
994
1084
|
signature=sig_text,
|
|
@@ -997,14 +1087,6 @@ def _extract_java(path: str, source: str) -> FileContract:
|
|
|
997
1087
|
return_type=None,
|
|
998
1088
|
))
|
|
999
1089
|
|
|
1000
|
-
# Import statements
|
|
1001
|
-
seen_sources: set[str] = set()
|
|
1002
|
-
for m in _JAVA_IMPORT_RE.finditer(source):
|
|
1003
|
-
full_import = m.group(1).strip()
|
|
1004
|
-
if full_import not in seen_sources:
|
|
1005
|
-
seen_sources.add(full_import)
|
|
1006
|
-
imports.append(ImportRecord(source=full_import, kind="named", symbols=[]))
|
|
1007
|
-
|
|
1008
1090
|
# External deps: top-2 package segments, skip java.* / javax.*
|
|
1009
1091
|
deps = sorted({
|
|
1010
1092
|
".".join(imp.source.split(".")[:2])
|
|
@@ -1021,6 +1103,7 @@ def _extract_java(path: str, source: str) -> FileContract:
|
|
|
1021
1103
|
functions=sorted(functions, key=lambda f: f.name)[:20],
|
|
1022
1104
|
types=sorted(types, key=lambda t: t.name),
|
|
1023
1105
|
dependencies=deps[:20],
|
|
1106
|
+
autowired_fields=autowired_fields[:20],
|
|
1024
1107
|
extraction_method="heuristic",
|
|
1025
1108
|
)
|
|
1026
1109
|
|
|
@@ -728,15 +728,13 @@ def main(
|
|
|
728
728
|
mode = "contract" # unknown → safe default
|
|
729
729
|
|
|
730
730
|
# Legacy flags imply raw mode unless --mode was explicitly overridden.
|
|
731
|
-
#
|
|
732
|
-
#
|
|
733
|
-
#
|
|
734
|
-
#
|
|
735
|
-
# force mode to raw. --agent is excluded: it now runs the contract pipeline
|
|
736
|
-
# and enriches contract_view with auto-enabled analyzers (deps, env, notes).
|
|
731
|
+
# --format yaml and --graph-modules are now compatible with contract_view:
|
|
732
|
+
# yaml is a serialization format (not an output-section flag)
|
|
733
|
+
# graph-modules output is included in contract_view when available
|
|
734
|
+
# Other flags that produce sections exclusive to standard_view still force raw.
|
|
737
735
|
_legacy_flags_active = (
|
|
738
|
-
compact or tree or
|
|
739
|
-
or docs or semantics or
|
|
736
|
+
compact or tree or trace_pipeline
|
|
737
|
+
or docs or semantics or full_metrics or architecture
|
|
740
738
|
)
|
|
741
739
|
if mode in ("contract", "standard") and _legacy_flags_active:
|
|
742
740
|
mode = "raw"
|
|
@@ -1106,6 +1104,17 @@ def main(
|
|
|
1106
1104
|
metrics_summary=metrics_summary,
|
|
1107
1105
|
)
|
|
1108
1106
|
|
|
1107
|
+
# Populate Java-specific root fields from java stack detection (FIX-6, 7, 8)
|
|
1108
|
+
_java_stack = next((s for s in stacks if s.stack == "java"), None)
|
|
1109
|
+
if _java_stack is not None:
|
|
1110
|
+
from dataclasses import replace as _dc_replace
|
|
1111
|
+
sm = _dc_replace(sm,
|
|
1112
|
+
packaging=getattr(_java_stack, "packaging", None) or None,
|
|
1113
|
+
language_version=getattr(_java_stack, "language_version", None) or None,
|
|
1114
|
+
spring_profiles=getattr(_java_stack, "spring_profiles", []) or [],
|
|
1115
|
+
app_server_hint=getattr(_java_stack, "app_server_hint", None) or None,
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1109
1118
|
# Semantic analysis (--semantics flag)
|
|
1110
1119
|
if semantic_analyzer is not None:
|
|
1111
1120
|
if workspace_analysis.workspaces:
|
|
@@ -1402,7 +1411,16 @@ def main(
|
|
|
1402
1411
|
if _is_contract_mode:
|
|
1403
1412
|
from sourcecode.contract_pipeline import ContractPipeline
|
|
1404
1413
|
from sourcecode.contract_model import ContractSummary as _ContractSummary
|
|
1405
|
-
|
|
1414
|
+
# FIX-1: Java projects need higher caps — many files, comprehensive coverage required
|
|
1415
|
+
_jvm_stacks = {"java", "kotlin", "scala", "groovy"}
|
|
1416
|
+
_is_jvm = any(s.stack in _jvm_stacks for s in sm.stacks)
|
|
1417
|
+
# FIX-1: Java projects need higher caps and no relevance threshold
|
|
1418
|
+
_max_files_cp = 2500 if _is_jvm else 500
|
|
1419
|
+
_cp = ContractPipeline(max_files=_max_files_cp)
|
|
1420
|
+
_java_pipeline_kwargs: dict = {}
|
|
1421
|
+
if _is_jvm:
|
|
1422
|
+
_java_pipeline_kwargs["max_contracts"] = 500
|
|
1423
|
+
_java_pipeline_kwargs["min_score"] = 0.0
|
|
1406
1424
|
try:
|
|
1407
1425
|
_contracts, _contract_summary = _cp.run(
|
|
1408
1426
|
target,
|
|
@@ -1420,6 +1438,7 @@ def main(
|
|
|
1420
1438
|
max_importers=max_importers,
|
|
1421
1439
|
semantic_calls=sm.semantic_calls or None,
|
|
1422
1440
|
code_notes=sm.code_notes or None,
|
|
1441
|
+
**_java_pipeline_kwargs,
|
|
1423
1442
|
)
|
|
1424
1443
|
except Exception as _exc:
|
|
1425
1444
|
typer.echo(f"[error] contract pipeline failed: {_exc}", err=True)
|
|
@@ -1462,7 +1481,22 @@ def main(
|
|
|
1462
1481
|
data = _contract_view(sm, emit_graph=emit_graph, depth=_depth)
|
|
1463
1482
|
if not no_redact:
|
|
1464
1483
|
data = redact_dict(data)
|
|
1465
|
-
|
|
1484
|
+
if format == "yaml":
|
|
1485
|
+
from io import StringIO
|
|
1486
|
+
from ruamel.yaml import YAML as _YAML
|
|
1487
|
+
_yaml = _YAML()
|
|
1488
|
+
_yaml.default_flow_style = False
|
|
1489
|
+
_yaml.representer.add_representer(
|
|
1490
|
+
type(None),
|
|
1491
|
+
lambda dumper, data_val: dumper.represent_scalar(
|
|
1492
|
+
"tag:yaml.org,2002:null", "null"
|
|
1493
|
+
),
|
|
1494
|
+
)
|
|
1495
|
+
_stream = StringIO()
|
|
1496
|
+
_yaml.dump(data, _stream)
|
|
1497
|
+
content = _stream.getvalue()
|
|
1498
|
+
else:
|
|
1499
|
+
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
1466
1500
|
elif agent:
|
|
1467
1501
|
data = agent_view(sm)
|
|
1468
1502
|
# When contract pipeline ran (mode=contract, no legacy flags), include
|
|
@@ -25,9 +25,13 @@ class ExportRecord:
|
|
|
25
25
|
"""Exported symbol."""
|
|
26
26
|
|
|
27
27
|
name: str
|
|
28
|
-
kind: str = "unknown" # function | class | const | type | default | react_component | enum | interface
|
|
28
|
+
kind: str = "unknown" # function | class | const | type | default | react_component | enum | interface | method
|
|
29
29
|
type_ref: Optional[str] = None
|
|
30
30
|
async_: bool = False
|
|
31
|
+
annotations: list[str] = field(default_factory=list) # Java: ["@Controller", "@Transactional"]
|
|
32
|
+
extends: Optional[str] = None # Java: parent class
|
|
33
|
+
implements: list[str] = field(default_factory=list) # Java: interfaces
|
|
34
|
+
signature: Optional[str] = None # Java method: full signature
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
@dataclass
|
|
@@ -96,6 +100,8 @@ class FileContract:
|
|
|
96
100
|
# Extraction quality
|
|
97
101
|
extraction_method: str = "heuristic" # ast | tree_sitter | heuristic
|
|
98
102
|
limitations: list[str] = field(default_factory=list)
|
|
103
|
+
# Java-specific (FIX-1)
|
|
104
|
+
autowired_fields: list[dict] = field(default_factory=list) # [{"type": "...", "name": "..."}]
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
@dataclass
|
|
@@ -182,6 +182,7 @@ class ContractPipeline:
|
|
|
182
182
|
semantic_calls: Optional[list] = None,
|
|
183
183
|
code_notes: Optional[list] = None,
|
|
184
184
|
max_contracts: Optional[int] = _MAX_CONTRACTS,
|
|
185
|
+
min_score: Optional[float] = None,
|
|
185
186
|
) -> tuple[list[FileContract], ContractSummary]:
|
|
186
187
|
"""Run the full extraction pipeline.
|
|
187
188
|
|
|
@@ -317,11 +318,17 @@ class ContractPipeline:
|
|
|
317
318
|
# 10. Top-N cap — enforce max_contracts when not in symbol-search mode.
|
|
318
319
|
# Symbol searches must return all matching files; budget applies only to
|
|
319
320
|
# the default architectural briefing use case.
|
|
321
|
+
_effective_min_score = min_score if min_score is not None else _MIN_CONTRACT_SCORE
|
|
320
322
|
if symbol is None and max_contracts is not None:
|
|
321
323
|
contracts = [
|
|
322
324
|
c for c in contracts
|
|
323
|
-
if c.relevance_score >=
|
|
325
|
+
if c.relevance_score >= _effective_min_score or c.is_entrypoint
|
|
324
326
|
][:max_contracts]
|
|
327
|
+
elif symbol is None and max_contracts is None:
|
|
328
|
+
contracts = [
|
|
329
|
+
c for c in contracts
|
|
330
|
+
if c.relevance_score >= _effective_min_score or c.is_entrypoint
|
|
331
|
+
]
|
|
325
332
|
|
|
326
333
|
# 11. Compress types if requested
|
|
327
334
|
if compress_types:
|
|
@@ -128,6 +128,8 @@ def _infer_role(name: str, ecosystem: str, scope: str) -> str:
|
|
|
128
128
|
return "runtime"
|
|
129
129
|
|
|
130
130
|
if ecosystem == "java":
|
|
131
|
+
if scope == "provided":
|
|
132
|
+
return "provided"
|
|
131
133
|
artifact = n.split(":")[-1] if ":" in n else n
|
|
132
134
|
if any(x in artifact for x in ("spring-boot", "spring-security")):
|
|
133
135
|
return "runtime"
|
|
@@ -1120,6 +1122,23 @@ class DependencyAnalyzer:
|
|
|
1120
1122
|
properties = self._parse_maven_properties(root_elem, ns)
|
|
1121
1123
|
dm_versions = self._parse_dependency_management(root_elem, ns, properties)
|
|
1122
1124
|
|
|
1125
|
+
# FIX-9: extract parent version for BOM resolution
|
|
1126
|
+
parent_elem = root_elem.find(f"{ns}parent")
|
|
1127
|
+
parent_version: Optional[str] = None
|
|
1128
|
+
parent_group: str = ""
|
|
1129
|
+
if parent_elem is not None:
|
|
1130
|
+
parent_version = (parent_elem.findtext(f"{ns}version") or "").strip() or None
|
|
1131
|
+
parent_group = (parent_elem.findtext(f"{ns}groupId") or "").strip()
|
|
1132
|
+
parent_artifact = (parent_elem.findtext(f"{ns}artifactId") or "").strip()
|
|
1133
|
+
# Propagate parent version into properties for ${project.parent.version}
|
|
1134
|
+
if parent_version:
|
|
1135
|
+
properties.setdefault("project.parent.version", parent_version)
|
|
1136
|
+
properties.setdefault("revision", parent_version)
|
|
1137
|
+
|
|
1138
|
+
# Infer packaging for FIX-6 (used by scope hint for provided)
|
|
1139
|
+
packaging_elem = root_elem.find(f"{ns}packaging")
|
|
1140
|
+
is_war = packaging_elem is not None and (packaging_elem.text or "").strip().lower() == "war"
|
|
1141
|
+
|
|
1123
1142
|
records: list[DependencyRecord] = []
|
|
1124
1143
|
deps_elem = root_elem.find(f"{ns}dependencies")
|
|
1125
1144
|
if deps_elem is None:
|
|
@@ -1134,14 +1153,36 @@ class DependencyAnalyzer:
|
|
|
1134
1153
|
declared = self._resolve_maven_version(version_raw, properties)
|
|
1135
1154
|
if declared is None:
|
|
1136
1155
|
declared = dm_versions.get(f"{group_id}:{artifact_id}")
|
|
1156
|
+
|
|
1157
|
+
# FIX-4: proper maven scope mapping
|
|
1137
1158
|
scope_text = (dep.findtext(f"{ns}scope") or "compile").strip().lower()
|
|
1138
|
-
|
|
1159
|
+
if scope_text == "test":
|
|
1160
|
+
scope = "dev"
|
|
1161
|
+
elif scope_text == "provided":
|
|
1162
|
+
scope = "provided"
|
|
1163
|
+
else:
|
|
1164
|
+
scope = "direct" # compile, runtime, system, import
|
|
1165
|
+
|
|
1166
|
+
# FIX-4: infer provided for embedded tomcat in WAR projects
|
|
1167
|
+
if (is_war and scope == "direct"
|
|
1168
|
+
and artifact_id in ("spring-boot-starter-tomcat", "tomcat-embed-core")):
|
|
1169
|
+
scope = "provided"
|
|
1170
|
+
|
|
1171
|
+
# FIX-9: resolve BOM version for Spring Boot / Spring Security starters
|
|
1172
|
+
resolved_version: Optional[str] = None
|
|
1173
|
+
if declared is None and parent_version:
|
|
1174
|
+
if group_id == "org.springframework.boot":
|
|
1175
|
+
resolved_version = parent_version
|
|
1176
|
+
elif group_id == "org.springframework.security" and "spring-security.version" in properties:
|
|
1177
|
+
resolved_version = properties["spring-security.version"]
|
|
1178
|
+
|
|
1139
1179
|
records.append(
|
|
1140
1180
|
DependencyRecord(
|
|
1141
1181
|
name=f"{group_id}:{artifact_id}",
|
|
1142
1182
|
ecosystem="java",
|
|
1143
1183
|
scope=scope,
|
|
1144
1184
|
declared_version=declared,
|
|
1185
|
+
resolved_version=resolved_version,
|
|
1145
1186
|
source="manifest",
|
|
1146
1187
|
manifest_path="pom.xml",
|
|
1147
1188
|
)
|
|
@@ -14,6 +14,8 @@ from sourcecode.detectors.parsers import read_text_lines, unique_strings
|
|
|
14
14
|
from sourcecode.schema import FrameworkDetection
|
|
15
15
|
from sourcecode.tree_utils import flatten_file_tree
|
|
16
16
|
|
|
17
|
+
_NS_TAG_RE = re.compile(r"\{[^}]+\}")
|
|
18
|
+
|
|
17
19
|
_MAX_FILE_SIZE = 256 * 1024 # 256 KB
|
|
18
20
|
_MAX_JAVA_ENTRY_SCAN = 1000
|
|
19
21
|
_MAX_ANNOTATION_ENTRY_POINTS = 500
|
|
@@ -50,14 +52,34 @@ class JavaDetector(AbstractDetector):
|
|
|
50
52
|
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
51
53
|
frameworks: list[FrameworkDetection] = []
|
|
52
54
|
manifests: list[str] = []
|
|
55
|
+
language_version: str | None = None
|
|
56
|
+
packaging: str | None = None
|
|
57
|
+
app_server_hint: str | None = None
|
|
58
|
+
spring_profiles: list[str] = []
|
|
53
59
|
|
|
54
60
|
if "pom.xml" in context.manifests:
|
|
55
61
|
manifests.append("pom.xml")
|
|
56
|
-
|
|
62
|
+
pom_path = context.root / "pom.xml"
|
|
63
|
+
frameworks.extend(self._frameworks_from_pom(pom_path))
|
|
64
|
+
meta = self._parse_pom_metadata(pom_path)
|
|
65
|
+
if meta.get("language_version"):
|
|
66
|
+
language_version = meta["language_version"]
|
|
67
|
+
if meta.get("packaging"):
|
|
68
|
+
packaging = meta["packaging"]
|
|
57
69
|
if "build.gradle" in context.manifests:
|
|
58
70
|
manifests.append("build.gradle")
|
|
59
71
|
frameworks.extend(self._frameworks_from_gradle(context.root / "build.gradle"))
|
|
60
72
|
|
|
73
|
+
# Detect app server from descriptor files
|
|
74
|
+
all_paths = flatten_file_tree(context.file_tree)
|
|
75
|
+
if any("weblogic.xml" in p or "weblogic-ejb-jar.xml" in p for p in all_paths):
|
|
76
|
+
app_server_hint = "weblogic"
|
|
77
|
+
elif any("wildfly" in p.lower() or "jboss" in p.lower() for p in all_paths):
|
|
78
|
+
app_server_hint = "wildfly"
|
|
79
|
+
|
|
80
|
+
# Spring profiles — check src/main/options/, src/main/resources/
|
|
81
|
+
spring_profiles = self._detect_spring_profiles(context.root, all_paths)
|
|
82
|
+
|
|
61
83
|
entry_points = self._collect_entry_points(context)
|
|
62
84
|
stack = StackDetection(
|
|
63
85
|
stack="java",
|
|
@@ -65,27 +87,124 @@ class JavaDetector(AbstractDetector):
|
|
|
65
87
|
confidence="high",
|
|
66
88
|
frameworks=self._dedupe_frameworks(frameworks),
|
|
67
89
|
manifests=manifests,
|
|
90
|
+
language_version=language_version,
|
|
91
|
+
packaging=packaging,
|
|
92
|
+
app_server_hint=app_server_hint,
|
|
93
|
+
spring_profiles=spring_profiles,
|
|
68
94
|
)
|
|
69
95
|
return [stack], entry_points
|
|
70
96
|
|
|
97
|
+
def _parse_pom_metadata(self, path: Path) -> dict:
|
|
98
|
+
"""Extract packaging, java version from pom.xml properties/parent."""
|
|
99
|
+
result: dict = {}
|
|
100
|
+
try:
|
|
101
|
+
tree = ElementTree.parse(path)
|
|
102
|
+
except (OSError, ElementTree.ParseError):
|
|
103
|
+
return result
|
|
104
|
+
root = tree.getroot()
|
|
105
|
+
ns_match = _NS_TAG_RE.match(root.tag)
|
|
106
|
+
ns = ns_match.group(0) if ns_match else ""
|
|
107
|
+
|
|
108
|
+
# Packaging (FIX-6)
|
|
109
|
+
packaging_elem = root.find(f"{ns}packaging")
|
|
110
|
+
if packaging_elem is not None and packaging_elem.text:
|
|
111
|
+
result["packaging"] = packaging_elem.text.strip().lower()
|
|
112
|
+
|
|
113
|
+
# Properties
|
|
114
|
+
props_elem = root.find(f"{ns}properties")
|
|
115
|
+
props: dict[str, str] = {}
|
|
116
|
+
if props_elem is not None:
|
|
117
|
+
for prop in props_elem:
|
|
118
|
+
tag = prop.tag.replace(ns, "") if ns else prop.tag
|
|
119
|
+
if prop.text:
|
|
120
|
+
props[tag] = prop.text.strip()
|
|
121
|
+
|
|
122
|
+
# Java version (FIX-7) — check properties first, then compiler plugin
|
|
123
|
+
for key in ("maven.compiler.source", "java.version", "maven.compiler.release"):
|
|
124
|
+
if key in props:
|
|
125
|
+
result["language_version"] = props[key]
|
|
126
|
+
break
|
|
127
|
+
if "language_version" not in result:
|
|
128
|
+
# Check maven-compiler-plugin configuration
|
|
129
|
+
for plugin in root.findall(f".//{ns}plugin"):
|
|
130
|
+
artifact = (plugin.findtext(f"{ns}artifactId") or "").strip()
|
|
131
|
+
if artifact == "maven-compiler-plugin":
|
|
132
|
+
config = plugin.find(f"{ns}configuration")
|
|
133
|
+
if config is not None:
|
|
134
|
+
for tag in ("source", "release"):
|
|
135
|
+
val = config.findtext(f"{ns}{tag}")
|
|
136
|
+
if val:
|
|
137
|
+
result["language_version"] = val.strip()
|
|
138
|
+
break
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def _detect_spring_profiles(self, root: Path, all_paths: list[str]) -> list[str]:
|
|
144
|
+
"""Detect Spring profiles from option/resource directories and application-{profile}.yml."""
|
|
145
|
+
profiles: list[str] = []
|
|
146
|
+
seen: set[str] = set()
|
|
147
|
+
|
|
148
|
+
# Pattern 1: src/main/options/{profile}/ directories
|
|
149
|
+
_PROFILE_DIRS = ("src/main/options/", "src/main/resources/")
|
|
150
|
+
for path in all_paths:
|
|
151
|
+
for prefix in _PROFILE_DIRS:
|
|
152
|
+
if path.startswith(prefix):
|
|
153
|
+
remainder = path[len(prefix):]
|
|
154
|
+
parts = remainder.split("/")
|
|
155
|
+
if len(parts) >= 1 and parts[0] and not parts[0].startswith("."):
|
|
156
|
+
candidate = parts[0]
|
|
157
|
+
# Only if it's a directory (has sub-paths) with application.yml
|
|
158
|
+
if candidate not in seen and not candidate.endswith(".yml") and not candidate.endswith(".yaml") and not candidate.endswith(".properties"):
|
|
159
|
+
seen.add(candidate)
|
|
160
|
+
profiles.append(candidate)
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
# Pattern 2: application-{profile}.yml files
|
|
164
|
+
_APP_PROFILE_RE = re.compile(r"application-([A-Za-z0-9_-]+)\.ya?ml$")
|
|
165
|
+
for path in all_paths:
|
|
166
|
+
m = _APP_PROFILE_RE.search(path)
|
|
167
|
+
if m:
|
|
168
|
+
profile = m.group(1)
|
|
169
|
+
if profile not in seen:
|
|
170
|
+
seen.add(profile)
|
|
171
|
+
profiles.append(profile)
|
|
172
|
+
|
|
173
|
+
# Filter out generic names that aren't profiles
|
|
174
|
+
_SKIP = frozenset({"test", "it", "integration"})
|
|
175
|
+
return [p for p in profiles if p.lower() not in _SKIP]
|
|
176
|
+
|
|
71
177
|
def _frameworks_from_pom(self, path: Path) -> list[FrameworkDetection]:
|
|
72
178
|
try:
|
|
73
179
|
tree = ElementTree.parse(path)
|
|
74
180
|
except (OSError, ElementTree.ParseError):
|
|
75
181
|
return []
|
|
76
|
-
|
|
77
|
-
|
|
182
|
+
root_elem = tree.getroot()
|
|
183
|
+
ns_match = _NS_TAG_RE.match(root_elem.tag)
|
|
184
|
+
ns = ns_match.group(0) if ns_match else ""
|
|
185
|
+
|
|
186
|
+
# Extract Spring Boot version from <parent> (FIX-3)
|
|
187
|
+
sb_version: str | None = None
|
|
188
|
+
parent_elem = root_elem.find(f"{ns}parent")
|
|
189
|
+
if parent_elem is not None:
|
|
190
|
+
parent_artifact = (parent_elem.findtext(f"{ns}artifactId") or "").strip()
|
|
191
|
+
if parent_artifact == "spring-boot-starter-parent":
|
|
192
|
+
sb_version = (parent_elem.findtext(f"{ns}version") or "").strip() or None
|
|
193
|
+
|
|
194
|
+
text = ElementTree.tostring(root_elem, encoding="unicode").lower()
|
|
195
|
+
frameworks = self._detect_jvm_frameworks(text, "pom.xml", sb_version=sb_version)
|
|
196
|
+
return frameworks
|
|
78
197
|
|
|
79
198
|
def _frameworks_from_gradle(self, path: Path) -> list[FrameworkDetection]:
|
|
80
199
|
content = "\n".join(read_text_lines(path)).lower()
|
|
81
200
|
return self._detect_jvm_frameworks(content, "build.gradle")
|
|
82
201
|
|
|
83
|
-
def _detect_jvm_frameworks(self, text: str, source: str) -> list[FrameworkDetection]:
|
|
202
|
+
def _detect_jvm_frameworks(self, text: str, source: str, *, sb_version: str | None = None) -> list[FrameworkDetection]:
|
|
84
203
|
frameworks: list[FrameworkDetection] = []
|
|
85
204
|
if "com.android.application" in text or "com.android.library" in text:
|
|
86
205
|
frameworks.append(FrameworkDetection(name="Android", source=source))
|
|
87
206
|
if "spring-boot" in text:
|
|
88
|
-
frameworks.append(FrameworkDetection(name="Spring Boot", source=source))
|
|
207
|
+
frameworks.append(FrameworkDetection(name="Spring Boot", source=source, version=sb_version))
|
|
89
208
|
if "spring-webmvc" in text or "spring-web" in text:
|
|
90
209
|
frameworks.append(FrameworkDetection(name="Spring MVC", source=source))
|
|
91
210
|
if "spring-webflux" in text:
|