"""Tests for the PHP belief-map parser (tree-sitter + regex fallback). Covers: class/interface/trait/enum extraction, PHP 8 attributes, heritage, trait usage inside class bodies, Symfony/Laravel constructor DI, PSR-4 import resolution, and sexp output format. """ import importlib.util import io import json import os import sys import tempfile import textwrap import unittest from contextlib import redirect_stdout from pathlib import Path SCRIPT_PATH = Path( os.environ.get( "BELIEF_SEARCH_PATH", Path(__file__).resolve().parent.parent / "scripts" / "build_belief_map.py", ) ) if not SCRIPT_PATH.exists(): SCRIPT_PATH = Path( "/Users/alexeus/.codex/skills/codespaces/scripts/belief_search.py" ) SPEC = importlib.util.spec_from_file_location("build_belief_map", SCRIPT_PATH) build_belief_map = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = build_belief_map SPEC.loader.exec_module(build_belief_map) PHP_SAMPLE = textwrap.dedent("""\ repo->find($id); } } interface FooInterface { public function getUser(int $id): mixed; } trait LoggerTrait { public function log(string $msg): void {} } enum Status: string { case Active = "active"; case Inactive = "inactive"; } function helper_function(): void {} """) class _PhpParserTestBase(unittest.TestCase): """Base class: parse PHP_SAMPLE and expose the FileResult.""" def _parse(self) -> "build_belief_map.FileResult": return self._parse_fn()( "/tmp/Test.php", PHP_SAMPLE, "app", 0.0, ) def _parse_fn(self): raise NotImplementedError class TreeSitterPhpTest(_PhpParserTestBase): """Tests for parse_php_treesitter -- only runs if tree-sitter-php is installed.""" @classmethod def setUpClass(cls): if build_belief_map._PHP_LANG is None: raise unittest.SkipTest("tree-sitter-php not installed") def _parse_fn(self): return build_belief_map.parse_php_treesitter def test_extracts_all_entity_types(self) -> None: """/* REQ-PHP-001: tree-sitter parser extracts class, interface, trait, enum, function. */""" r = self._parse() kinds = {e["name"]: e["kind"] for e in r.entities} self.assertEqual(kinds["UserService"], "class") self.assertEqual(kinds["FooInterface"], "interface") self.assertEqual(kinds["LoggerTrait"], "trait") self.assertEqual(kinds["Status"], "enum") self.assertEqual(kinds["helper_function"], "function") def test_interface_and_trait_are_abstract(self) -> None: """/* REQ-PHP-002: interfaces and traits are added to exports_abstract. */""" r = self._parse() self.assertIn("FooInterface", r.exports_abstract) self.assertIn("LoggerTrait", r.exports_abstract) def test_php8_attributes_captured_as_decorators(self) -> None: """/* REQ-PHP-003: PHP 8 #[Attribute] is captured in entity decorators. */""" r = self._parse() svc = next(e for e in r.entities if e["name"] == "UserService") self.assertIn("AsService", svc.get("decorators", [])) def test_heritage_extraction(self) -> None: """/* REQ-PHP-004: implements clause produces bases on the class entity. */""" r = self._parse() svc = next(e for e in r.entities if e["name"] == "UserService") self.assertIn("FooInterface", svc.get("bases", [])) def test_trait_usage_inside_class(self) -> None: """/* REQ-PHP-005: use TraitX; inside a class body populates traits_used. */""" r = self._parse() self.assertIn("LoggerTrait", r.traits_used) def test_constructor_di_extraction(self) -> None: """/* REQ-PHP-006: constructor type-hints produce implements entries for DI edges. */""" r = self._parse() # UserRepo is the short name from \App\Repositories\UserRepo self.assertIn("UserRepo", r.implements) def test_imports_extracted_from_use_statements(self) -> None: """/* REQ-PHP-007: use statements populate imports with FQCNs. */""" r = self._parse() self.assertIn("App\\Contracts\\FooInterface", r.imports) self.assertIn("App\\Traits\\LoggerTrait", r.imports) def test_methods_extracted(self) -> None: """/* REQ-PHP-008: public methods appear in entity :methods. */""" r = self._parse() svc = next(e for e in r.entities if e["name"] == "UserService") self.assertIn("getUser", svc.get("methods", [])) # __construct is filtered out self.assertNotIn("__construct", svc.get("methods", [])) class RegexFallbackPhpTest(_PhpParserTestBase): """Tests for parse_php_regex -- the fallback when tree-sitter-php is absent.""" def _parse_fn(self): return build_belief_map.parse_php_regex def test_extracts_class_interface_trait_enum(self) -> None: """/* REQ-PHP-009: regex fallback extracts basic entity types. */""" r = self._parse() kinds = {e["name"]: e["kind"] for e in r.entities} self.assertEqual(kinds.get("UserService"), "class") self.assertEqual(kinds.get("FooInterface"), "interface") self.assertEqual(kinds.get("LoggerTrait"), "trait") self.assertEqual(kinds.get("Status"), "enum") def test_trait_usage_detected(self) -> None: """/* REQ-PHP-010: regex fallback detects trait usage inside class body. */""" r = self._parse() self.assertIn("LoggerTrait", r.traits_used) def test_imports_extracted(self) -> None: """/* REQ-PHP-011: regex fallback extracts use-statement imports. */""" r = self._parse() self.assertTrue( any("FooInterface" in imp for imp in r.imports), f"Expected FooInterface in imports: {r.imports}", ) class Psr4ResolutionTest(unittest.TestCase): """End-to-end test: build a mini PHP project with composer.json and verify edges.""" def setUp(self) -> None: self.tmpdir = tempfile.TemporaryDirectory() self.addCleanup(self.tmpdir.cleanup) root = self.root = self.tmpdir.name Path(root, "composer.json").write_text( json.dumps( { "autoload": {"psr-4": {"App\\": "src/"}}, } ), encoding="utf-8", ) Path(root, "src", "Controller").mkdir(parents=True) Path(root, "src", "Model").mkdir(parents=True) Path(root, "src", "Trait").mkdir(parents=True) Path(root, "src", "Controller", "UserController.php").write_text( textwrap.dedent("""\ None: """/* REQ-PHP-012: PSR-4 autoload resolution creates IMPORTS edges between PHP modules. */""" nodes, edges = build_belief_map.build_graph( [ build_belief_map.parse_file( ( str(Path(self.root, "src", "Controller", "UserController.php")), "php", "app", ) ), build_belief_map.parse_file( (str(Path(self.root, "src", "Model", "User.php")), "php", "app") ), build_belief_map.parse_file( ( str(Path(self.root, "src", "Trait", "LoggerTrait.php")), "php", "app", ) ), ], self.root, ) imports_edges = [e for e in edges if e["type"] == "IMPORTS"] targets = {e["target"] for e in imports_edges} # UserController should import User and LoggerTrait node_ids = {n["id"] for n in nodes} self.assertTrue( any("User" in t for t in targets), f"Expected User in import targets: {targets}", ) self.assertTrue( any("LoggerTrait" in t for t in targets), f"Expected LoggerTrait in import targets: {targets}", ) def test_trait_usage_creates_calls_api_edge(self) -> None: """/* REQ-PHP-013: trait usage inside a class creates a CALLS_API edge. */""" nodes, edges = build_belief_map.build_graph( [ build_belief_map.parse_file( ( str(Path(self.root, "src", "Controller", "UserController.php")), "php", "app", ) ), build_belief_map.parse_file( (str(Path(self.root, "src", "Model", "User.php")), "php", "app") ), build_belief_map.parse_file( ( str(Path(self.root, "src", "Trait", "LoggerTrait.php")), "php", "app", ) ), ], self.root, ) calls_api_edges = [e for e in edges if e["type"] == "CALLS_API"] targets = {e["target"] for e in calls_api_edges} self.assertTrue( any("LoggerTrait" in t for t in targets), f"Expected LoggerTrait in CALLS_API targets: {targets}", ) def test_sexp_output_contains_php_lang_and_trait_tag(self) -> None: """/* REQ-PHP-014: sexp output uses 'php' lang tag and 'trt' entity tag for traits. */""" results = [ build_belief_map.parse_file( ( str(Path(self.root, "src", "Controller", "UserController.php")), "php", "app", ) ), build_belief_map.parse_file( (str(Path(self.root, "src", "Model", "User.php")), "php", "app") ), build_belief_map.parse_file( (str(Path(self.root, "src", "Trait", "LoggerTrait.php")), "php", "app") ), ] nodes, edges = build_belief_map.build_graph(results, self.root) out_path = str(Path(self.root, ".belief_map.sexp")) build_belief_map.write_sexp( out_path, nodes, edges, self.root, "test", len(results) ) content = Path(out_path).read_text(encoding="utf-8") self.assertIn(" php ", content, "Expected (node ... php ...) in sexp output") self.assertIn( "(trt ", content, "Expected (trt ...) entity tag for trait in sexp output" ) if __name__ == "__main__": unittest.main()