mod common;
use common::sqry_bin;
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use std::path::Path;
use tempfile::TempDir;
#[test]
fn cli_php_callers_instance_methods() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class UserService {
public function authenticate($username, $password) {
$user = $this->findUser($username);
if ($user) {
return $this->validatePassword($user, $password);
}
return false;
}
private function findUser($username) {
return Database::query('SELECT * FROM users WHERE username = ?', [$username]);
}
private function validatePassword($user, $password) {
return password_verify($password, $user->password_hash);
}
}
class Database {
public static function query($sql, $params = []) {
return null;
}
}
"#;
std::fs::write(project.path().join("UserService.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("callers:findUser")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("authenticate"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:validatePassword")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("authenticate"));
}
#[test]
fn cli_php_callers_static_methods() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class Logger {
public static function info($message) {
self::write('INFO', $message);
}
public static function error($message) {
self::write('ERROR', $message);
}
private static function write($level, $message) {
echo "[$level] $message\n";
}
}
class Application {
public function run() {
Logger::info('Application started');
Logger::error('Something went wrong');
}
}
"#;
std::fs::write(project.path().join("Logger.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("callers:info")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("run"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:error")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("run"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:write")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("info"))
.stdout(predicate::str::contains("error"));
}
#[test]
fn cli_php_callers_global_functions() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
function normalizeUsername($username) {
return trim(strtolower($username));
}
function processUserInput($username, $email) {
$username = normalizeUsername($username);
$valid = validateEmail($email);
return $valid;
}
"#;
std::fs::write(project.path().join("helpers.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("callers:validateEmail")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("processUserInput"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:normalizeUsername")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("processUserInput"));
}
#[test]
fn cli_php_callers_namespaced_functions() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
namespace App\Helpers;
function slugify($text) {
return strtolower(preg_replace('/[^a-z0-9]+/', '-', $text));
}
function truncate($text, $length = 100) {
return strlen($text) > $length ? substr($text, 0, $length) . '...' : $text;
}
namespace App\Services;
use function App\Helpers\slugify;
class PostService {
public function createSlug($title) {
return slugify($title);
}
}
"#;
std::fs::write(project.path().join("services.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("callers:slugify")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("createSlug"));
}
#[test]
fn cli_php_callers_null_safe_operator() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class User {
private $profile;
public function getProfile() {
return $this->profile;
}
}
class Profile {
private $name;
public function getName() {
return $this->name;
}
}
class UserService {
public function getUserName($user) {
return $user?->getProfile()?->getName();
}
}
"#;
std::fs::write(project.path().join("user.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("callers:getProfile")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("getUserName"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:getName")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("getUserName"));
}
#[test]
fn cli_php_exports_classes_and_interfaces() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class User {
public function getId() {
return $this->id;
}
}
interface Repository {
public function findById($id);
public function save($entity);
}
class UserRepository implements Repository {
public function findById($id) {
return null;
}
public function save($entity) {
return true;
}
}
trait Timestampable {
public function getCreatedAt() {
return $this->created_at;
}
}
"#;
std::fs::write(project.path().join("models.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:User")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("models.php"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:Repository")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("models.php"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:Timestampable")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("models.php"));
}
#[test]
fn cli_php_exports_visibility_filtering() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class UserService {
public function createUser($username, $password) {
$this->validateInput($username, $password);
$hash = $this->hashPassword($password);
return true;
}
public function updateUser($id, $data) {
return true;
}
protected function validateInput($username, $password) {
return true;
}
private function hashPassword($password) {
return password_hash($password, PASSWORD_DEFAULT);
}
}
"#;
std::fs::write(project.path().join("UserService.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:createUser")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("UserService.php"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:updateUser")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("UserService.php"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:validateInput")
.arg(project.path())
.assert()
.success()
.stderr(predicate::str::contains("No matches found"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:hashPassword")
.arg(project.path())
.assert()
.success()
.stderr(predicate::str::contains("No matches found"));
}
#[test]
fn cli_php_exports_namespaced_classes() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
namespace App\Services;
class UserService {
public function getUsers() {
return [];
}
}
namespace App\Repositories;
class UserRepository {
public function findAll() {
return [];
}
}
"#;
std::fs::write(project.path().join("app.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:UserService")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("app.php"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:UserRepository")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("app.php"));
}
#[test]
fn cli_php_exports_global_functions() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
function globalHelper() {
return true;
}
namespace App\Helpers;
function namespacedHelper() {
return true;
}
"#;
std::fs::write(project.path().join("functions.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:globalHelper")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("functions.php"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:namespacedHelper")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("functions.php"));
}
#[test]
fn cli_php_imports_use_statements() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
namespace App\Controllers;
use App\Services\UserService;
use App\Repositories\UserRepository;
class UserController {
private $userService;
public function __construct(UserService $service) {
$this->userService = $service;
}
public function index() {
return $this->userService->getUsers();
}
}
"#;
std::fs::write(project.path().join("UserController.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("imports:UserService")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("UserController.php"));
Command::new(sqry_bin())
.arg("query")
.arg("imports:UserRepository")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("UserController.php"));
}
#[test]
fn cli_php_imports_require_statements() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
require_once 'config.php';
require_once __DIR__ . '/helpers.php';
include 'optional.php';
class Application {
public function run() {
return true;
}
}
"#;
std::fs::write(project.path().join("app.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("imports:config")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("app.php"));
Command::new(sqry_bin())
.arg("query")
.arg("imports:helpers")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("app.php"));
}
#[test]
fn cli_php_callees_query() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class UserService {
public function authenticate($username, $password) {
$user = $this->findUser($username);
$valid = $this->validatePassword($user, $password);
$this->logAttempt($username, $valid);
return $valid;
}
private function findUser($username) {
return null;
}
private function validatePassword($user, $password) {
return password_verify($password, $user->password);
}
private function logAttempt($username, $success) {
error_log("Login attempt: $username");
}
}
"#;
std::fs::write(project.path().join("UserService.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("callees:authenticate")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("findUser"))
.stdout(predicate::str::contains("validatePassword"))
.stdout(predicate::str::contains("logAttempt"));
}
#[test]
fn cli_php_callers_method_chaining() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class QueryBuilder {
public function table($table) {
return $this;
}
public function where($column, $value) {
return $this;
}
public function orderBy($column) {
return $this;
}
public function get() {
return [];
}
}
class UserQuery {
private $builder;
public function __construct() {
$this->builder = new QueryBuilder();
}
public function findActiveUsers() {
return $this->builder
->table('users')
->where('status', 'active')
->orderBy('created_at')
->get();
}
}
"#;
std::fs::write(project.path().join("QueryBuilder.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("callers:table")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("findActiveUsers"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:where")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("findActiveUsers"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:get")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("findActiveUsers"));
}
#[test]
fn cli_php_json_exports_with_methods() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
namespace App\Services;
class UserService {
public function authenticate($username, $password) {
return $this->findUser($username);
}
private function findUser($username) {
return null;
}
protected function validatePassword($password) {
return strlen($password) >= 8;
}
}
"#;
std::fs::write(project.path().join("UserService.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
let output = Command::new(sqry_bin())
.arg("query")
.arg("exports:UserService")
.arg("--json")
.arg(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let json_str = String::from_utf8(output).expect("valid UTF-8");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("valid JSON");
let results = json["results"].as_array().expect("results should be array");
assert!(
!results.is_empty(),
"exports:UserService should find the class"
);
let class_result = results
.iter()
.find(|r| {
r["name"]
.as_str()
.map(|n| n.contains("UserService"))
.unwrap_or(false)
})
.expect("Should find UserService class");
assert_eq!(
class_result["kind"].as_str(),
Some("class"),
"Result should be a class"
);
let output = Command::new(sqry_bin())
.arg("query")
.arg("exports:authenticate")
.arg("--json")
.arg(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let json_str = String::from_utf8(output).expect("valid UTF-8");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("valid JSON");
let results = json["results"].as_array().expect("results should be array");
assert!(
!results.is_empty(),
"exports:authenticate should find the public method"
);
let method_result = results
.iter()
.find(|r| {
r["qualified_name"]
.as_str()
.map(|n| n.contains("authenticate"))
.unwrap_or(false)
})
.expect("Should find authenticate method");
assert_eq!(
method_result["kind"].as_str(),
Some("method"),
"Result should be a method"
);
}
#[test]
fn cli_php_json_calls_with_dynamic_metadata() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
namespace App\Services;
class EventDispatcher {
public function dispatch($eventName, $data) {
// Dynamic call via call_user_func
$handler = $this->getHandler($eventName);
call_user_func($handler, $data);
// Regular static method call
Logger::info("Event dispatched: " . $eventName);
}
private function getHandler($eventName) {
return 'handleEvent';
}
}
"#;
std::fs::write(project.path().join("EventDispatcher.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
let output = Command::new(sqry_bin())
.arg("query")
.arg("callers:call_user_func")
.arg("--json")
.arg(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let json_str = String::from_utf8(output).expect("valid UTF-8");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("valid JSON");
let results = json["results"].as_array().expect("results should be array");
assert!(json.get("query").is_some(), "JSON should have 'query' key");
assert!(json.get("stats").is_some(), "JSON should have 'stats' key");
if !results.is_empty() {
let first_result = &results[0];
assert!(
first_result.get("file_path").is_some(),
"Result should have file_path"
);
assert!(
first_result.get("start_line").is_some(),
"Result should have start_line"
);
}
}
#[test]
fn cli_php_callers_query_no_results() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class Foo {
public function bar() {
return 42;
}
}
"#;
std::fs::write(project.path().join("Foo.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
let output = Command::new(sqry_bin())
.arg("query")
.arg("callers:nonexistent")
.arg(project.path())
.assert()
.success();
let stdout = String::from_utf8_lossy(&output.get_output().stdout);
assert!(
stdout.contains("No results") || stdout.is_empty() || stdout.trim().is_empty(),
"Should indicate no results when querying for non-existent symbol"
);
}
#[test]
fn cli_php_exports_empty_class() {
let project = TempDir::new().unwrap();
let php_code = r#"<?php
class EmptyClass {
// No methods
}
"#;
std::fs::write(project.path().join("Empty.php"), php_code).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:EmptyClass")
.arg(project.path())
.assert()
.success();
}
#[test]
fn cli_php_callers_syntax_error_file() {
let project = TempDir::new().unwrap();
let valid_php = r#"<?php
class Valid {
public function test() {
return 1;
}
}
"#;
std::fs::write(project.path().join("Valid.php"), valid_php).unwrap();
let invalid_php = r#"<?php
class Invalid {
public function broken( { // Syntax error
return 1;
}
}
"#;
std::fs::write(project.path().join("Invalid.php"), invalid_php).unwrap();
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:Valid")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("Valid"));
}
#[test]
#[serial]
fn cli_php_fixture_symfony_exports_recall() {
let fixture_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("sqry-lang-php/tests/fixtures/relations/php/integration");
let _ = std::fs::remove_dir_all(fixture_path.join(".sqry-index"));
let _ = std::fs::remove_file(fixture_path.join(".sqry-index"));
let _ = std::fs::remove_file(fixture_path.join(".sqry-index.trg"));
let _ = std::fs::remove_dir_all(fixture_path.join(".sqry-cache"));
Command::new(sqry_bin())
.arg("index")
.arg(&fixture_path)
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:UserController")
.arg(&fixture_path)
.assert()
.success()
.stdout(predicate::str::contains("UserController"));
let expected_methods = vec!["index", "show", "create", "edit", "delete", "search"];
let mut found_count = 0;
for method in &expected_methods {
let output = Command::new(sqry_bin())
.arg("query")
.arg(format!("exports:{}", method))
.arg(&fixture_path)
.output()
.expect("command should run");
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains(method) {
found_count += 1;
}
}
assert!(
found_count >= 5,
"Expected to find at least 5/6 exported methods, found {}. Expected: {:?}",
found_count,
expected_methods
);
}
#[test]
#[serial]
fn cli_php_fixture_symfony_callers_recall() {
let fixture_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("sqry-lang-php/tests/fixtures/relations/php/integration");
let _ = std::fs::remove_dir_all(fixture_path.join(".sqry-index"));
let _ = std::fs::remove_file(fixture_path.join(".sqry-index"));
let _ = std::fs::remove_file(fixture_path.join(".sqry-index.trg"));
let _ = std::fs::remove_dir_all(fixture_path.join(".sqry-cache"));
Command::new(sqry_bin())
.arg("index")
.arg(&fixture_path)
.assert()
.success();
let output = Command::new(sqry_bin())
.arg("query")
.arg("callers:render")
.arg(&fixture_path)
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(output).expect("valid UTF-8");
let expected_callers = vec!["index", "show", "create", "edit"];
let mut found_count = 0;
for caller in &expected_callers {
if stdout.contains(caller) {
found_count += 1;
}
}
assert!(
found_count >= 3,
"Expected to find at least 3/4 callers of render, found {}",
found_count
);
}
#[test]
fn cli_php_fixture_laravel_comprehensive() {
let fixture_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("sqry-lang-php/tests/fixtures/relations/php/integration/laravel_service.php");
let project = TempDir::new().unwrap();
std::fs::copy(&fixture_path, project.path().join("laravel_service.php")).expect("copy fixture");
Command::new(sqry_bin())
.arg("index")
.arg(project.path())
.assert()
.success();
Command::new(sqry_bin())
.arg("query")
.arg("exports:UserService")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("UserService"));
Command::new(sqry_bin())
.arg("query")
.arg("exports:createUser")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("createUser"));
Command::new(sqry_bin())
.arg("query")
.arg("callers:validateUserData")
.arg(project.path())
.assert()
.success()
.stdout(predicate::str::contains("createUser"));
}