sourcecode 0.23.0__tar.gz → 0.24.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.23.0 → sourcecode-0.24.0}/PKG-INFO +1 -1
- {sourcecode-0.23.0 → sourcecode-0.24.0}/docs/schema.md +2 -2
- {sourcecode-0.23.0 → sourcecode-0.24.0}/pyproject.toml +1 -1
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/architecture_summary.py +50 -0
- sourcecode-0.24.0/src/sourcecode/detectors/csproj_parser.py +203 -0
- sourcecode-0.24.0/src/sourcecode/detectors/dotnet.py +182 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/graph_analyzer.py +80 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/schema.py +1 -1
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_dependency_analyzer_node_python.py +1 -1
- sourcecode-0.24.0/tests/test_detector_dotnet.py +482 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration.py +1 -1
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_dependencies.py +1 -1
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_schema.py +2 -2
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_signal_hierarchy.py +1 -1
- sourcecode-0.23.0/src/sourcecode/detectors/dotnet.py +0 -57
- {sourcecode-0.23.0 → sourcecode-0.24.0}/.gitignore +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/.ruff.toml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/README.md +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/__init__.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/conftest.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_cli.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.24.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.24.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.24.0",
|
|
649
649
|
"analyzed_path": "/abs/path/to/project"
|
|
650
650
|
},
|
|
651
651
|
"file_tree": {
|
|
@@ -74,6 +74,8 @@ class ArchitectureSummarizer:
|
|
|
74
74
|
lang_lines = self._summarize_go_entry(entry_point.path, content)
|
|
75
75
|
elif suffix in _JAVA_EXTENSIONS:
|
|
76
76
|
lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
|
|
77
|
+
elif suffix in {".cs", ".fs", ".vb"}:
|
|
78
|
+
lang_lines = self._summarize_dotnet_entry(sm.stacks)
|
|
77
79
|
else:
|
|
78
80
|
lang_lines = []
|
|
79
81
|
|
|
@@ -189,6 +191,44 @@ class ArchitectureSummarizer:
|
|
|
189
191
|
lines.append("Orquesta el arranque de la aplicacion JVM.")
|
|
190
192
|
return lines
|
|
191
193
|
|
|
194
|
+
def _summarize_dotnet_entry(self, stacks: list[StackDetection]) -> list[str]:
|
|
195
|
+
dotnet_stacks = [s for s in stacks if s.stack == "dotnet"]
|
|
196
|
+
if not dotnet_stacks:
|
|
197
|
+
return []
|
|
198
|
+
lines: list[str] = []
|
|
199
|
+
signals = [sig for s in dotnet_stacks for sig in s.signals]
|
|
200
|
+
|
|
201
|
+
for sig in signals:
|
|
202
|
+
if "project" in sig and "detected" in sig:
|
|
203
|
+
lines.append(sig[0].upper() + sig[1:] + ".")
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
for sig in signals:
|
|
207
|
+
if sig.startswith("project types:"):
|
|
208
|
+
types = sig.removeprefix("project types:").strip()
|
|
209
|
+
lines.append(f"Stack: {types}.")
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
for sig in signals:
|
|
213
|
+
if sig.startswith("target frameworks:"):
|
|
214
|
+
fws = sig.removeprefix("target frameworks:").strip()
|
|
215
|
+
lines.append(f"Target: {fws}.")
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
for sig in signals:
|
|
219
|
+
if sig.startswith("architecture:"):
|
|
220
|
+
pattern = sig.removeprefix("architecture:").strip()
|
|
221
|
+
lines.append(f"Patrón detectado: {pattern}.")
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
framework_names = [f.name for s in dotnet_stacks for f in s.frameworks]
|
|
225
|
+
if framework_names:
|
|
226
|
+
lines.append(f"Frameworks: {', '.join(framework_names)}.")
|
|
227
|
+
|
|
228
|
+
if not lines:
|
|
229
|
+
lines.append("Solución .NET detectada.")
|
|
230
|
+
return lines
|
|
231
|
+
|
|
192
232
|
def _summarize_go_entry(self, path: str, content: str) -> list[str]:
|
|
193
233
|
imports = re.findall(r'"([^"]+)"', content)
|
|
194
234
|
internal = [module for module in imports if not module.startswith(("fmt", "net/", "os", "context"))]
|
|
@@ -273,6 +313,16 @@ class ArchitectureSummarizer:
|
|
|
273
313
|
confidence="medium",
|
|
274
314
|
)
|
|
275
315
|
)
|
|
316
|
+
elif path.endswith("Program.cs") or path.endswith("Program.fs"):
|
|
317
|
+
candidates.append(
|
|
318
|
+
EntryPoint(
|
|
319
|
+
path=path,
|
|
320
|
+
stack="dotnet",
|
|
321
|
+
kind="cli",
|
|
322
|
+
source="convention",
|
|
323
|
+
confidence="medium",
|
|
324
|
+
)
|
|
325
|
+
)
|
|
276
326
|
return candidates
|
|
277
327
|
|
|
278
328
|
def _fallback_priority(self, path: str) -> tuple[int, int, str]:
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CsprojProject:
|
|
10
|
+
name: str
|
|
11
|
+
path: str # repo-relative path to .csproj
|
|
12
|
+
project_dir: str # repo-relative folder (empty if root)
|
|
13
|
+
target_frameworks: list[str] = field(default_factory=list)
|
|
14
|
+
output_type: str = ""
|
|
15
|
+
sdk: str = ""
|
|
16
|
+
project_references: list[str] = field(default_factory=list) # resolved repo-relative csproj paths
|
|
17
|
+
package_references: list[tuple[str, str]] = field(default_factory=list)
|
|
18
|
+
project_type: str = "classlib" # webapi|classlib|console|test|blazor|worker
|
|
19
|
+
language: str = "csharp" # csharp|fsharp|vbnet
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_TEST_PACKAGES: frozenset[str] = frozenset({
|
|
23
|
+
"xunit", "nunit", "mstest.testframework", "microsoft.net.test.sdk",
|
|
24
|
+
"nunit3testadapter", "xunit.runner.visualstudio", "mstest.testadapter",
|
|
25
|
+
"coverlet.collector", "moq", "nsubstitute", "fluentassertions",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
_LAYER_KEYWORDS: tuple[tuple[str, str], ...] = (
|
|
29
|
+
("api", "Api"),
|
|
30
|
+
("web", "Api"),
|
|
31
|
+
("endpoint", "Api"),
|
|
32
|
+
("controller", "Api"),
|
|
33
|
+
("application", "Application"),
|
|
34
|
+
("usecase", "Application"),
|
|
35
|
+
("usecases", "Application"),
|
|
36
|
+
("domain", "Domain"),
|
|
37
|
+
("core", "Domain"),
|
|
38
|
+
("infrastructure", "Infrastructure"),
|
|
39
|
+
("infra", "Infrastructure"),
|
|
40
|
+
("persistence", "Infrastructure"),
|
|
41
|
+
("repository", "Infrastructure"),
|
|
42
|
+
("shared", "Shared"),
|
|
43
|
+
("common", "Shared"),
|
|
44
|
+
("contracts", "Shared"),
|
|
45
|
+
("abstractions", "Shared"),
|
|
46
|
+
("test", "Tests"),
|
|
47
|
+
("tests", "Tests"),
|
|
48
|
+
("spec", "Tests"),
|
|
49
|
+
("specs", "Tests"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_csproj(absolute_path: Path, relative_path: str) -> CsprojProject | None:
|
|
54
|
+
"""Parse a .csproj/.fsproj/.vbproj and return structured project info. Returns None on error."""
|
|
55
|
+
try:
|
|
56
|
+
content = absolute_path.read_text(encoding="utf-8", errors="replace")
|
|
57
|
+
root_elem = ET.fromstring(content)
|
|
58
|
+
except (OSError, ET.ParseError):
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
suffix = Path(relative_path).suffix.lower()
|
|
62
|
+
language = {"fsproj": "fsharp", "vbproj": "vbnet"}.get(suffix.lstrip("."), "csharp")
|
|
63
|
+
|
|
64
|
+
project_dir = str(PurePosixPath(relative_path).parent)
|
|
65
|
+
if project_dir == ".":
|
|
66
|
+
project_dir = ""
|
|
67
|
+
|
|
68
|
+
name = Path(relative_path).stem
|
|
69
|
+
sdk = root_elem.get("Sdk", "") or ""
|
|
70
|
+
|
|
71
|
+
frameworks: list[str] = []
|
|
72
|
+
output_type = ""
|
|
73
|
+
package_refs: list[tuple[str, str]] = []
|
|
74
|
+
project_refs: list[str] = []
|
|
75
|
+
|
|
76
|
+
for elem in root_elem.iter():
|
|
77
|
+
tag = _strip_ns(elem.tag)
|
|
78
|
+
text = (elem.text or "").strip()
|
|
79
|
+
|
|
80
|
+
if tag == "TargetFramework":
|
|
81
|
+
if not frameworks and text:
|
|
82
|
+
frameworks = [text]
|
|
83
|
+
elif tag == "TargetFrameworks":
|
|
84
|
+
if text:
|
|
85
|
+
frameworks = [f.strip() for f in text.split(";") if f.strip()]
|
|
86
|
+
elif tag == "OutputType":
|
|
87
|
+
if not output_type:
|
|
88
|
+
output_type = text
|
|
89
|
+
elif tag == "AssemblyName":
|
|
90
|
+
if text:
|
|
91
|
+
name = text
|
|
92
|
+
elif tag == "PackageReference":
|
|
93
|
+
pkg = elem.get("Include", "") or ""
|
|
94
|
+
ver = elem.get("Version", "") or (elem.findtext("Version") or "").strip()
|
|
95
|
+
if pkg:
|
|
96
|
+
package_refs.append((pkg, ver))
|
|
97
|
+
elif tag == "ProjectReference":
|
|
98
|
+
include = (elem.get("Include", "") or "").replace("\\", "/")
|
|
99
|
+
if include:
|
|
100
|
+
project_refs.append(include)
|
|
101
|
+
|
|
102
|
+
if not sdk:
|
|
103
|
+
sdk = _detect_sdk_from_imports(root_elem)
|
|
104
|
+
|
|
105
|
+
resolved_refs = [
|
|
106
|
+
r for r in (_resolve_ref(relative_path, ref) for ref in project_refs) if r
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
project_type = _classify_project(
|
|
110
|
+
sdk=sdk,
|
|
111
|
+
output_type=output_type,
|
|
112
|
+
name=name,
|
|
113
|
+
package_refs=[p[0] for p in package_refs],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return CsprojProject(
|
|
117
|
+
name=name,
|
|
118
|
+
path=relative_path,
|
|
119
|
+
project_dir=project_dir,
|
|
120
|
+
target_frameworks=frameworks,
|
|
121
|
+
output_type=output_type,
|
|
122
|
+
sdk=sdk,
|
|
123
|
+
project_references=resolved_refs,
|
|
124
|
+
package_references=package_refs,
|
|
125
|
+
project_type=project_type,
|
|
126
|
+
language=language,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def infer_architecture_pattern(projects: list[CsprojProject]) -> str | None:
|
|
131
|
+
"""Infer Clean Architecture / Onion / Layered from project names."""
|
|
132
|
+
layers: set[str] = set()
|
|
133
|
+
for project in projects:
|
|
134
|
+
name_lower = project.name.lower()
|
|
135
|
+
for keyword, layer in _LAYER_KEYWORDS:
|
|
136
|
+
if keyword in name_lower:
|
|
137
|
+
layers.add(layer)
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if {"Application", "Domain", "Infrastructure"} <= layers:
|
|
141
|
+
return "Clean Architecture"
|
|
142
|
+
if {"Api", "Domain", "Infrastructure"} <= layers:
|
|
143
|
+
return "Onion Architecture"
|
|
144
|
+
if {"Api", "Application", "Infrastructure"} <= layers:
|
|
145
|
+
return "Layered Architecture"
|
|
146
|
+
if len(layers) >= 3:
|
|
147
|
+
return "Layered Architecture"
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _strip_ns(tag: str) -> str:
|
|
152
|
+
return tag.split("}", 1)[1] if "}" in tag else tag
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _resolve_ref(source_csproj: str, ref: str) -> str | None:
|
|
156
|
+
"""Resolve a ProjectReference path (relative to source .csproj) to a repo-relative path."""
|
|
157
|
+
source_dir = str(PurePosixPath(source_csproj).parent)
|
|
158
|
+
base = "" if source_dir == "." else source_dir
|
|
159
|
+
combined = f"{base}/{ref}" if base else ref
|
|
160
|
+
parts: list[str] = []
|
|
161
|
+
for part in combined.split("/"):
|
|
162
|
+
if part == "..":
|
|
163
|
+
if parts:
|
|
164
|
+
parts.pop()
|
|
165
|
+
elif part and part != ".":
|
|
166
|
+
parts.append(part)
|
|
167
|
+
return "/".join(parts) if parts else None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _detect_sdk_from_imports(root_elem: ET.Element) -> str:
|
|
171
|
+
for elem in root_elem.iter():
|
|
172
|
+
if _strip_ns(elem.tag) == "Import":
|
|
173
|
+
project = elem.get("Project", "")
|
|
174
|
+
if "Microsoft.NET" in project:
|
|
175
|
+
return project
|
|
176
|
+
return ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _classify_project(
|
|
180
|
+
*,
|
|
181
|
+
sdk: str,
|
|
182
|
+
output_type: str,
|
|
183
|
+
name: str,
|
|
184
|
+
package_refs: list[str],
|
|
185
|
+
) -> str:
|
|
186
|
+
sdk_l = sdk.lower()
|
|
187
|
+
name_l = name.lower()
|
|
188
|
+
pkgs_l = {p.lower() for p in package_refs}
|
|
189
|
+
out_l = output_type.lower()
|
|
190
|
+
|
|
191
|
+
if pkgs_l & _TEST_PACKAGES or any(kw in name_l for kw in ("test", "tests", "spec", "specs")):
|
|
192
|
+
return "test"
|
|
193
|
+
if "microsoft.net.sdk.blazor" in sdk_l or any("blazor" in p for p in pkgs_l):
|
|
194
|
+
return "blazor"
|
|
195
|
+
if "microsoft.net.sdk.web" in sdk_l or any(
|
|
196
|
+
p.startswith("microsoft.aspnetcore") for p in pkgs_l
|
|
197
|
+
):
|
|
198
|
+
return "webapi"
|
|
199
|
+
if "microsoft.net.sdk.worker" in sdk_l or "microsoft.extensions.hosting" in pkgs_l:
|
|
200
|
+
return "worker"
|
|
201
|
+
if out_l in ("exe", "winexe"):
|
|
202
|
+
return "console"
|
|
203
|
+
return "classlib"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
from sourcecode.detectors.base import (
|
|
6
|
+
AbstractDetector,
|
|
7
|
+
DetectionContext,
|
|
8
|
+
EntryPoint,
|
|
9
|
+
StackDetection,
|
|
10
|
+
)
|
|
11
|
+
from sourcecode.detectors.csproj_parser import (
|
|
12
|
+
CsprojProject,
|
|
13
|
+
infer_architecture_pattern,
|
|
14
|
+
parse_csproj,
|
|
15
|
+
)
|
|
16
|
+
from sourcecode.schema import FrameworkDetection
|
|
17
|
+
from sourcecode.tree_utils import flatten_file_tree, path_exists_in_tree
|
|
18
|
+
|
|
19
|
+
_PROJECT_EXTENSIONS = (".csproj", ".fsproj", ".vbproj")
|
|
20
|
+
|
|
21
|
+
_FRAMEWORK_SIGNALS: tuple[tuple[str, str], ...] = (
|
|
22
|
+
("microsoft.net.sdk.web", "ASP.NET Core"),
|
|
23
|
+
("microsoft.net.sdk.blazor", "Blazor"),
|
|
24
|
+
("microsoft.net.sdk.worker", "Worker Service"),
|
|
25
|
+
("microsoft.aspnetcore", "ASP.NET Core"),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_ENTRY_CANDIDATES = (
|
|
29
|
+
"Program.cs",
|
|
30
|
+
"src/Program.cs",
|
|
31
|
+
"Program.fs",
|
|
32
|
+
"src/Program.fs",
|
|
33
|
+
"Startup.cs",
|
|
34
|
+
"src/Startup.cs",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DotnetDetector(AbstractDetector):
|
|
39
|
+
name = "dotnet"
|
|
40
|
+
priority = 32
|
|
41
|
+
|
|
42
|
+
def can_detect(self, context: DetectionContext) -> bool:
|
|
43
|
+
return any(
|
|
44
|
+
path.endswith((*_PROJECT_EXTENSIONS, ".sln"))
|
|
45
|
+
for path in flatten_file_tree(context.file_tree)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
49
|
+
all_paths = flatten_file_tree(context.file_tree)
|
|
50
|
+
project_paths = [p for p in all_paths if p.endswith(_PROJECT_EXTENSIONS)]
|
|
51
|
+
|
|
52
|
+
projects: list[CsprojProject] = []
|
|
53
|
+
for rel_path in project_paths:
|
|
54
|
+
project = parse_csproj(context.root / rel_path, rel_path)
|
|
55
|
+
if project is not None:
|
|
56
|
+
projects.append(project)
|
|
57
|
+
|
|
58
|
+
frameworks = self._collect_frameworks(projects)
|
|
59
|
+
signals = self._build_signals(projects)
|
|
60
|
+
entry_points = self._detect_entry_points(context, projects, frameworks)
|
|
61
|
+
|
|
62
|
+
manifests = project_paths + [p for p in all_paths if p.endswith(".sln")]
|
|
63
|
+
stack = StackDetection(
|
|
64
|
+
stack="dotnet",
|
|
65
|
+
detection_method="manifest",
|
|
66
|
+
confidence="high",
|
|
67
|
+
frameworks=frameworks,
|
|
68
|
+
package_manager="nuget",
|
|
69
|
+
manifests=manifests,
|
|
70
|
+
signals=signals,
|
|
71
|
+
)
|
|
72
|
+
return [stack], entry_points
|
|
73
|
+
|
|
74
|
+
def _collect_frameworks(self, projects: list[CsprojProject]) -> list[FrameworkDetection]:
|
|
75
|
+
seen: dict[str, FrameworkDetection] = {}
|
|
76
|
+
for project in projects:
|
|
77
|
+
content_lower = project.sdk.lower()
|
|
78
|
+
for pkg_name, _ in project.package_references:
|
|
79
|
+
content_lower += " " + pkg_name.lower()
|
|
80
|
+
for signal, framework_name in _FRAMEWORK_SIGNALS:
|
|
81
|
+
if signal in content_lower and framework_name not in seen:
|
|
82
|
+
seen[framework_name] = FrameworkDetection(name=framework_name, source="manifest")
|
|
83
|
+
return list(seen.values())
|
|
84
|
+
|
|
85
|
+
def _build_signals(self, projects: list[CsprojProject]) -> list[str]:
|
|
86
|
+
if not projects:
|
|
87
|
+
return []
|
|
88
|
+
signals: list[str] = []
|
|
89
|
+
|
|
90
|
+
count = len(projects)
|
|
91
|
+
signals.append(f"{count} project{'s' if count != 1 else ''} detected")
|
|
92
|
+
|
|
93
|
+
type_counter: Counter[str] = Counter(p.project_type for p in projects)
|
|
94
|
+
type_parts = []
|
|
95
|
+
for ptype, n in type_counter.most_common():
|
|
96
|
+
type_parts.append(f"{ptype}×{n}" if n > 1 else ptype)
|
|
97
|
+
signals.append(f"project types: {', '.join(type_parts)}")
|
|
98
|
+
|
|
99
|
+
frameworks_seen: set[str] = set()
|
|
100
|
+
all_frameworks: list[str] = []
|
|
101
|
+
for project in projects:
|
|
102
|
+
for fw in project.target_frameworks:
|
|
103
|
+
if fw and fw not in frameworks_seen:
|
|
104
|
+
frameworks_seen.add(fw)
|
|
105
|
+
all_frameworks.append(fw)
|
|
106
|
+
if all_frameworks:
|
|
107
|
+
signals.append(f"target frameworks: {', '.join(all_frameworks[:4])}")
|
|
108
|
+
|
|
109
|
+
pattern = infer_architecture_pattern(projects)
|
|
110
|
+
if pattern:
|
|
111
|
+
signals.append(f"architecture: {pattern}")
|
|
112
|
+
|
|
113
|
+
return signals
|
|
114
|
+
|
|
115
|
+
def _detect_entry_points(
|
|
116
|
+
self,
|
|
117
|
+
context: DetectionContext,
|
|
118
|
+
projects: list[CsprojProject],
|
|
119
|
+
frameworks: list[FrameworkDetection],
|
|
120
|
+
) -> list[EntryPoint]:
|
|
121
|
+
entry_points: list[EntryPoint] = []
|
|
122
|
+
framework_names = {f.name for f in frameworks}
|
|
123
|
+
has_web = bool(framework_names & {"ASP.NET Core", "Blazor"})
|
|
124
|
+
|
|
125
|
+
# Entry points from exe/web projects
|
|
126
|
+
for project in projects:
|
|
127
|
+
if project.project_type in ("webapi", "console", "worker", "blazor"):
|
|
128
|
+
kind = _project_type_to_entry_kind(project.project_type, has_web)
|
|
129
|
+
# Look for Program.cs/Startup.cs relative to the project dir
|
|
130
|
+
for candidate in _entry_file_candidates(project):
|
|
131
|
+
if path_exists_in_tree(context.file_tree, candidate):
|
|
132
|
+
entry_points.append(
|
|
133
|
+
EntryPoint(
|
|
134
|
+
path=candidate,
|
|
135
|
+
stack="dotnet",
|
|
136
|
+
kind=kind,
|
|
137
|
+
source="manifest",
|
|
138
|
+
confidence="high",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Fallback: known entry file patterns
|
|
144
|
+
if not entry_points:
|
|
145
|
+
kind = "api" if has_web else "cli"
|
|
146
|
+
for candidate in _ENTRY_CANDIDATES:
|
|
147
|
+
if path_exists_in_tree(context.file_tree, candidate):
|
|
148
|
+
entry_points.append(
|
|
149
|
+
EntryPoint(
|
|
150
|
+
path=candidate,
|
|
151
|
+
stack="dotnet",
|
|
152
|
+
kind=kind,
|
|
153
|
+
source="manifest",
|
|
154
|
+
confidence="medium",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
return entry_points
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _project_type_to_entry_kind(project_type: str, has_web: bool) -> str:
|
|
163
|
+
if project_type == "webapi":
|
|
164
|
+
return "api"
|
|
165
|
+
if project_type == "blazor":
|
|
166
|
+
return "web"
|
|
167
|
+
if project_type == "worker":
|
|
168
|
+
return "server"
|
|
169
|
+
return "cli"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _entry_file_candidates(project: CsprojProject) -> list[str]:
|
|
173
|
+
prefix = f"{project.project_dir}/" if project.project_dir else ""
|
|
174
|
+
candidates = [
|
|
175
|
+
f"{prefix}Program.cs",
|
|
176
|
+
f"{prefix}Program.fs",
|
|
177
|
+
f"{prefix}Startup.cs",
|
|
178
|
+
]
|
|
179
|
+
# Also check without prefix for root-level projects
|
|
180
|
+
if prefix:
|
|
181
|
+
candidates += ["Program.cs", "src/Program.cs"]
|
|
182
|
+
return candidates
|
|
@@ -21,6 +21,7 @@ class GraphAnalyzer:
|
|
|
21
21
|
_NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
22
22
|
_GO_EXTENSIONS = {".go"}
|
|
23
23
|
_JVM_EXTENSIONS = {".java", ".kt", ".scala"}
|
|
24
|
+
_DOTNET_PROJECT_EXTENSIONS = {".csproj", ".fsproj", ".vbproj"}
|
|
24
25
|
_SUPPORTED_EXTENSIONS = _PYTHON_EXTENSIONS | _NODE_EXTENSIONS | _GO_EXTENSIONS | _JVM_EXTENSIONS
|
|
25
26
|
_DEFAULT_EDGE_KINDS: dict[GraphDetail, tuple[str, ...]] = {
|
|
26
27
|
"high": ("imports",),
|
|
@@ -167,6 +168,23 @@ class GraphAnalyzer:
|
|
|
167
168
|
go_imports = self._build_go_import_map(root, source_files)
|
|
168
169
|
jvm_types = self._build_jvm_type_map(root, source_files)
|
|
169
170
|
|
|
171
|
+
# .NET: project-level graph from .csproj/.fsproj/.vbproj manifests
|
|
172
|
+
all_tree_paths = [p for p in flatten_file_tree(file_tree)]
|
|
173
|
+
dotnet_project_paths = [
|
|
174
|
+
p for p in all_tree_paths
|
|
175
|
+
if Path(p).suffix.lower() in self._DOTNET_PROJECT_EXTENSIONS
|
|
176
|
+
and (root / p).is_file()
|
|
177
|
+
]
|
|
178
|
+
if dotnet_project_paths:
|
|
179
|
+
dn_nodes, dn_edges, dn_lims = self._analyze_dotnet_projects(
|
|
180
|
+
root, dotnet_project_paths, workspace
|
|
181
|
+
)
|
|
182
|
+
for node in dn_nodes:
|
|
183
|
+
self._append_node(nodes, node_ids, node)
|
|
184
|
+
for edge in dn_edges:
|
|
185
|
+
self._append_edge(edges, edge_keys, edge)
|
|
186
|
+
limitations.extend(dn_lims)
|
|
187
|
+
|
|
170
188
|
for relative_path in source_files:
|
|
171
189
|
absolute_path = root / relative_path
|
|
172
190
|
try:
|
|
@@ -1144,6 +1162,62 @@ class GraphAnalyzer:
|
|
|
1144
1162
|
edge_keys.add(key)
|
|
1145
1163
|
edges.append(edge)
|
|
1146
1164
|
|
|
1165
|
+
def _analyze_dotnet_projects(
|
|
1166
|
+
self,
|
|
1167
|
+
root: Path,
|
|
1168
|
+
csproj_paths: list[str],
|
|
1169
|
+
workspace: str | None,
|
|
1170
|
+
) -> tuple[list[GraphNode], list[GraphEdge], list[str]]:
|
|
1171
|
+
from sourcecode.detectors.csproj_parser import parse_csproj
|
|
1172
|
+
|
|
1173
|
+
nodes: list[GraphNode] = []
|
|
1174
|
+
edges: list[GraphEdge] = []
|
|
1175
|
+
limitations: list[str] = []
|
|
1176
|
+
|
|
1177
|
+
projects = []
|
|
1178
|
+
node_id_map: dict[str, str] = {} # csproj_path → node_id
|
|
1179
|
+
|
|
1180
|
+
for rel_path in csproj_paths:
|
|
1181
|
+
project = parse_csproj(root / rel_path, rel_path)
|
|
1182
|
+
if project is None:
|
|
1183
|
+
limitations.append(f"dotnet_parse_error:{rel_path}")
|
|
1184
|
+
continue
|
|
1185
|
+
projects.append(project)
|
|
1186
|
+
node_path = project.project_dir if project.project_dir else project.name
|
|
1187
|
+
node_id = f"module:{node_path}"
|
|
1188
|
+
node_id_map[project.path] = node_id
|
|
1189
|
+
nodes.append(
|
|
1190
|
+
GraphNode(
|
|
1191
|
+
id=node_id,
|
|
1192
|
+
kind="module",
|
|
1193
|
+
language=project.language,
|
|
1194
|
+
path=node_path,
|
|
1195
|
+
display_name=project.name,
|
|
1196
|
+
workspace=workspace,
|
|
1197
|
+
)
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
for project in projects:
|
|
1201
|
+
source_id = node_id_map.get(project.path)
|
|
1202
|
+
if source_id is None:
|
|
1203
|
+
continue
|
|
1204
|
+
for ref_path in project.project_references:
|
|
1205
|
+
target_id = node_id_map.get(ref_path)
|
|
1206
|
+
if target_id is None:
|
|
1207
|
+
limitations.append(f"dotnet_unresolved_ref:{project.path}:{ref_path}")
|
|
1208
|
+
continue
|
|
1209
|
+
edges.append(
|
|
1210
|
+
GraphEdge(
|
|
1211
|
+
source=source_id,
|
|
1212
|
+
target=target_id,
|
|
1213
|
+
kind="imports",
|
|
1214
|
+
confidence="high",
|
|
1215
|
+
method="heuristic",
|
|
1216
|
+
)
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
return nodes, edges, limitations
|
|
1220
|
+
|
|
1147
1221
|
def _language_for_suffix(self, suffix: str) -> str:
|
|
1148
1222
|
if suffix in self._PYTHON_EXTENSIONS:
|
|
1149
1223
|
return "python"
|
|
@@ -1157,6 +1231,12 @@ class GraphAnalyzer:
|
|
|
1157
1231
|
return "kotlin"
|
|
1158
1232
|
if suffix == ".scala":
|
|
1159
1233
|
return "scala"
|
|
1234
|
+
if suffix == ".cs":
|
|
1235
|
+
return "csharp"
|
|
1236
|
+
if suffix == ".fs":
|
|
1237
|
+
return "fsharp"
|
|
1238
|
+
if suffix == ".vb":
|
|
1239
|
+
return "vbnet"
|
|
1160
1240
|
return "unknown"
|
|
1161
1241
|
|
|
1162
1242
|
def _node_id(self, kind: str, path: str, symbol: str | None = None) -> str:
|
|
@@ -527,6 +527,6 @@ class SourceMap:
|
|
|
527
527
|
code_notes: list[CodeNote] = field(default_factory=list)
|
|
528
528
|
code_adrs: list[AdrRecord] = field(default_factory=list)
|
|
529
529
|
code_notes_summary: Optional[CodeNotesSummary] = None
|
|
530
|
-
# Confidence & Explainability (v0.
|
|
530
|
+
# Confidence & Explainability (v0.24.0)
|
|
531
531
|
confidence_summary: Optional[ConfidenceSummary] = None
|
|
532
532
|
analysis_gaps: list[AnalysisGap] = field(default_factory=list)
|
|
@@ -106,6 +106,6 @@ def test_python_requirements_without_lockfile_keeps_declared_versions(tmp_path:
|
|
|
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.24.0"
|
|
110
110
|
assert typer_dep.resolved_version is None
|
|
111
111
|
assert rich_dep.declared_version == ">=13.0"
|