Extract packages, project refs, and target framework from a .csproj/.fsproj/.vbproj.
(path: Path)
| 14287 | |
| 14288 | |
| 14289 | def extract_csproj(path: Path) -> dict: |
| 14290 | """Extract packages, project refs, and target framework from a .csproj/.fsproj/.vbproj.""" |
| 14291 | import xml.etree.ElementTree as ET |
| 14292 | |
| 14293 | try: |
| 14294 | src = path.read_bytes() |
| 14295 | except OSError: |
| 14296 | return {"nodes": [], "edges": [], "error": f"cannot read {path}"} |
| 14297 | |
| 14298 | if len(src) > _PROJECT_XML_MAX_BYTES: |
| 14299 | return {"nodes": [], "edges": [], "error": "project file too large"} |
| 14300 | if not _project_xml_is_safe(src): |
| 14301 | return {"nodes": [], "edges": [], |
| 14302 | "error": "refusing XML with DOCTYPE/ENTITY declaration"} |
| 14303 | |
| 14304 | try: |
| 14305 | tree = ET.fromstring(src) |
| 14306 | except ET.ParseError as e: |
| 14307 | return {"nodes": [], "edges": [], "error": f"XML parse error: {e}"} |
| 14308 | |
| 14309 | file_nid = _make_id(str(path)) |
| 14310 | str_path = str(path) |
| 14311 | nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code", |
| 14312 | "source_file": str_path, "source_location": None}] |
| 14313 | edges: list[dict] = [] |
| 14314 | seen_ids: set[str] = set() |
| 14315 | seen_ids.add(file_nid) |
| 14316 | |
| 14317 | ns = "" |
| 14318 | root_tag = tree.tag |
| 14319 | if root_tag.startswith("{"): |
| 14320 | ns = root_tag.split("}")[0] + "}" |
| 14321 | |
| 14322 | def find_all(tag: str): |
| 14323 | return tree.iter(f"{ns}{tag}") |
| 14324 | |
| 14325 | for tf in find_all("TargetFramework"): |
| 14326 | if tf.text: |
| 14327 | fw_nid = _make_id("framework", tf.text.strip()) |
| 14328 | if fw_nid and fw_nid not in seen_ids: |
| 14329 | seen_ids.add(fw_nid) |
| 14330 | nodes.append({"id": fw_nid, "label": tf.text.strip(), |
| 14331 | "file_type": "concept", "source_file": str_path, |
| 14332 | "source_location": None}) |
| 14333 | edges.append({"source": file_nid, "target": fw_nid, |
| 14334 | "relation": "references", "confidence": "EXTRACTED", |
| 14335 | "source_file": str_path, "weight": 1.0}) |
| 14336 | |
| 14337 | for tf in find_all("TargetFrameworks"): |
| 14338 | if tf.text: |
| 14339 | for fw in tf.text.strip().split(";"): |
| 14340 | fw = fw.strip() |
| 14341 | if fw: |
| 14342 | fw_nid = _make_id("framework", fw) |
| 14343 | if fw_nid and fw_nid not in seen_ids: |
| 14344 | seen_ids.add(fw_nid) |
| 14345 | nodes.append({"id": fw_nid, "label": fw, |
| 14346 | "file_type": "concept", "source_file": str_path, |