Regex-based fallback for Spock spec files where tree-sitter-groovy cannot parse ``def "feature name"()`` methods. Merges import edges from the tree-sitter pass (which survive reliably) with class and feature-method nodes extracted via regex.
(path: Path, ts_result: dict)
| 6356 | |
| 6357 | |
| 6358 | def _extract_spock_fallback(path: Path, ts_result: dict) -> dict: |
| 6359 | """Regex-based fallback for Spock spec files where tree-sitter-groovy cannot parse |
| 6360 | ``def "feature name"()`` methods. Merges import edges from the tree-sitter pass |
| 6361 | (which survive reliably) with class and feature-method nodes extracted via regex. |
| 6362 | """ |
| 6363 | import re as _re |
| 6364 | source = path.read_text(errors="replace") |
| 6365 | str_path = str(path) |
| 6366 | stem = _file_stem(path) |
| 6367 | |
| 6368 | # Only keep the file node from the tree-sitter pass (guaranteed present and |
| 6369 | # correctly IDed) plus all import edges. All other ts nodes are discarded to |
| 6370 | # avoid orphaned method/constructor nodes whose parent edges were dropped. |
| 6371 | file_node = next((n for n in ts_result.get("nodes", []) if n.get("label") == path.name), None) |
| 6372 | nodes: list[dict] = [file_node] if file_node else [] |
| 6373 | edges: list[dict] = [e for e in ts_result.get("edges", []) if e.get("context") == "import"] |
| 6374 | seen_ids: set[str] = {n["id"] for n in nodes} |
| 6375 | |
| 6376 | def _add_node(nid: str, label: str, line: int) -> None: |
| 6377 | if nid not in seen_ids: |
| 6378 | seen_ids.add(nid) |
| 6379 | nodes.append({ |
| 6380 | "id": nid, |
| 6381 | "label": label, |
| 6382 | "file_type": "code", |
| 6383 | "source_file": str_path, |
| 6384 | "source_location": f"L{line}", |
| 6385 | }) |
| 6386 | |
| 6387 | def _add_edge(src: str, tgt: str, relation: str, line: int, |
| 6388 | confidence: str = "EXTRACTED") -> None: |
| 6389 | edges.append({ |
| 6390 | "source": src, |
| 6391 | "target": tgt, |
| 6392 | "relation": relation, |
| 6393 | "confidence": confidence, |
| 6394 | "source_file": str_path, |
| 6395 | "source_location": f"L{line}", |
| 6396 | "weight": 1.0, |
| 6397 | }) |
| 6398 | |
| 6399 | lines_text = source.splitlines() |
| 6400 | |
| 6401 | # Extract class declarations |
| 6402 | class_re = _re.compile(r"^\s*(?:[\w@]+\s+)*class\s+(\w+)") |
| 6403 | # Extract Spock feature methods: def "..." () or def '...' () |
| 6404 | # Two separate capture groups per quote style so apostrophes inside |
| 6405 | # double-quoted names (e.g. "shouldn't") are captured correctly. |
| 6406 | feature_re = _re.compile(r"""^\s*def\s+(?:\"([^\"]+)\"|'([^']+)')\s*\(""") |
| 6407 | # Extract plain def methods (non-string names) as well |
| 6408 | plain_method_re = _re.compile(r"""^\s*def\s+(\w+)\s*\(""") |
| 6409 | |
| 6410 | current_class_nid: str | None = None |
| 6411 | file_nid = _make_id(str_path) |
| 6412 | |
| 6413 | # Ensure the file node exists (tree-sitter pass may have emitted it) |
| 6414 | if file_nid not in seen_ids: |
| 6415 | _add_node(file_nid, path.name, 1) |
no test coverage detected