1use std::collections::{HashMap, HashSet};
18use std::ops::ControlFlow;
19use std::path::{Path, PathBuf};
20use std::sync::{Arc, LazyLock};
21
22use mir_codebase::storage::StubSlice;
23use php_ast::ast::ExprKind;
24use php_ast::visitor::{walk_expr, walk_program, Visitor};
25use php_lexer::TokenKind;
26use rayon::prelude::*;
27
28use crate::db::MirDb;
29use crate::php_version::PhpVersion;
30
31include!(concat!(env!("OUT_DIR"), "/stub_files.rs"));
33
34include!(concat!(env!("OUT_DIR"), "/phpstorm_builtin_fns.rs"));
37
38pub(crate) static ESSENTIAL_STUB_PATHS: &[&str] = &[
47 "stubs/Core/Core.php",
48 "stubs/Core/Core_c.php",
49 "stubs/Core/Core_d.php",
50 "stubs/SPL/SPL.php",
51 "stubs/SPL/SPL_c1.php",
52 "stubs/SPL/SPL_f.php",
53 "stubs/date/date.php",
54 "stubs/date/date_c.php",
55 "stubs/date/date_d.php",
56 "stubs/standard/_standard_manual.php",
57 "stubs/standard/_types.php",
58 "stubs/standard/basic.php",
59 "stubs/standard/password.php",
60 "stubs/standard/standard_0.php",
61 "stubs/standard/standard_1.php",
62 "stubs/standard/standard_10.php",
63 "stubs/standard/standard_2.php",
64 "stubs/standard/standard_3.php",
65 "stubs/standard/standard_4.php",
66 "stubs/standard/standard_5.php",
67 "stubs/standard/standard_6.php",
68 "stubs/standard/standard_7.php",
69 "stubs/standard/standard_8.php",
70 "stubs/standard/standard_9.php",
71 "stubs/standard/standard_defines.php",
72];
73
74pub(crate) fn stub_content_for_path(path: &str) -> Option<&'static str> {
78 STUB_FILES
79 .iter()
80 .find_map(|&(p, c)| if p == path { Some(c) } else { None })
81}
82
83pub(crate) fn stub_path_for_function(name: &str) -> Option<&'static str> {
87 let lower = name.to_ascii_lowercase();
88 STUB_FN_INDEX
89 .binary_search_by(|(k, _)| (*k).cmp(lower.as_str()))
90 .ok()
91 .map(|i| STUB_FN_INDEX[i].1)
92}
93
94pub(crate) fn stub_path_for_class(fqcn: &str) -> Option<&'static str> {
98 let trimmed = fqcn.strip_prefix('\\').unwrap_or(fqcn);
99 let lower = trimmed.to_ascii_lowercase();
100 STUB_CLASS_INDEX
101 .binary_search_by(|(k, _)| (*k).cmp(lower.as_str()))
102 .ok()
103 .map(|i| STUB_CLASS_INDEX[i].1)
104}
105
106pub(crate) fn stub_path_for_constant(name: &str) -> Option<&'static str> {
109 let trimmed = name.strip_prefix('\\').unwrap_or(name);
110 STUB_CONST_INDEX
111 .binary_search_by(|(k, _)| (*k).cmp(trimmed))
112 .ok()
113 .map(|i| STUB_CONST_INDEX[i].1)
114}
115
116pub(crate) fn collect_referenced_builtin_paths(source: &str) -> Vec<&'static str> {
126 use php_lexer::lex_all;
127
128 let mut tokens: HashSet<&str> = HashSet::new();
129 let (lexed, _errors) = lex_all(source);
130
131 let mut i = 0;
132 while i < lexed.len() {
133 let token = &lexed[i];
134 if token.kind == TokenKind::Identifier {
135 let start = token.span.start as usize;
136 let end = token.span.end as usize;
137 if let Some(mut text) = source.get(start..end) {
138 let mut j = i + 1;
140 while j + 1 < lexed.len()
141 && lexed[j].kind == TokenKind::Backslash
142 && lexed[j + 1].kind == TokenKind::Identifier
143 {
144 j += 2;
145 if let Some(part) = source.get(start..(lexed[j - 1].span.end as usize)) {
146 text = part;
147 }
148 }
149 tokens.insert(text);
150 i = j;
151 } else {
152 i += 1;
153 }
154 } else {
155 i += 1;
156 }
157 }
158
159 let mut paths: HashSet<&'static str> = HashSet::new();
160 for token in tokens {
161 if let Some(p) = stub_path_for_function(token) {
162 paths.insert(p);
163 }
164 if let Some(p) = stub_path_for_class(token) {
165 paths.insert(p);
166 }
167 if let Some(p) = stub_path_for_constant(token) {
168 paths.insert(p);
169 }
170 }
171 paths.into_iter().collect()
172}
173
174pub(crate) fn collect_referenced_builtin_paths_from_ast(
183 program: &php_ast::ast::Program<'_, '_>,
184) -> Vec<&'static str> {
185 let mut visitor = BuiltinRefVisitor {
186 paths: HashSet::new(),
187 };
188 let _ = walk_program(&mut visitor, program);
189 visitor.paths.into_iter().collect()
190}
191
192struct BuiltinRefVisitor {
194 paths: HashSet<&'static str>,
195}
196
197impl<'arena, 'src> Visitor<'arena, 'src> for BuiltinRefVisitor {
198 fn visit_expr(&mut self, expr: &php_ast::ast::Expr<'arena, 'src>) -> ControlFlow<()> {
199 match &expr.kind {
200 ExprKind::FunctionCall(call) => {
201 if let ExprKind::Identifier(name) = &call.name.kind {
202 if let Some(p) = stub_path_for_function(name.as_str()) {
203 self.paths.insert(p);
204 }
205 }
206 }
207 ExprKind::New(new_expr) => {
208 if let ExprKind::Identifier(name) = &new_expr.class.kind {
209 if let Some(p) = stub_path_for_class(name.as_str()) {
210 self.paths.insert(p);
211 }
212 }
213 }
214 ExprKind::StaticMethodCall(call) => {
215 if let ExprKind::Identifier(name) = &call.class.kind {
216 if let Some(p) = stub_path_for_class(name.as_str()) {
217 self.paths.insert(p);
218 }
219 }
220 }
221 ExprKind::ClassConstAccess(access) => {
222 if let ExprKind::Identifier(name) = &access.class.kind {
223 if let Some(p) = stub_path_for_class(name.as_str()) {
224 self.paths.insert(p);
225 }
226 }
227 }
228 ExprKind::Identifier(name) => {
229 if let Some(p) = stub_path_for_constant(name.as_str()) {
230 self.paths.insert(p);
231 }
232 }
233 _ => {}
234 }
235 walk_expr(self, expr)
236 }
237}
238
239pub fn stub_files() -> &'static [(&'static str, &'static str)] {
247 STUB_FILES
248}
249
250#[allow(dead_code)]
258pub(crate) fn load_stubs(db: &mut MirDb) {
259 load_stubs_for_version(db, PhpVersion::LATEST);
260}
261
262#[allow(dead_code)]
267pub(crate) fn load_stubs_for_version(db: &mut MirDb, php_version: PhpVersion) {
268 for slice in builtin_stub_slices_for_version(php_version) {
269 db.ingest_stub_slice(&slice);
270 }
271}
272
273pub(crate) fn builtin_stub_slices_for_version(php_version: PhpVersion) -> Vec<StubSlice> {
274 STUB_FILES
275 .par_iter()
276 .map(|(filename, content)| stub_slice_from_source(filename, content, Some(php_version)))
277 .collect()
278}
279
280pub(crate) fn user_stub_slices(files: &[PathBuf], dirs: &[PathBuf]) -> Vec<StubSlice> {
281 let mut all_paths: Vec<PathBuf> = files.to_vec();
282 for dir in dirs {
283 collect_stub_dir_paths(dir, &mut all_paths);
284 }
285
286 all_paths
287 .par_iter()
288 .filter_map(|path| parse_stub_file_slice(path))
289 .collect()
290}
291
292pub(crate) fn stub_slice_from_source(
293 filename: &str,
294 content: &str,
295 php_version: Option<PhpVersion>,
296) -> StubSlice {
297 let arena = crate::arena::create_parse_arena(content.len());
298 let result = php_rs_parser::parse(&arena, content);
299 let file: Arc<str> = Arc::from(filename);
300 let collector =
301 crate::collector::DefinitionCollector::new_for_slice(file, content, &result.source_map);
302 let collector = match php_version {
303 Some(version) => collector.with_php_version(version),
304 None => collector,
305 };
306 let (slice, _) = collector.collect_slice(&result.program);
307 slice
308}
309
310fn parse_stub_file_slice(path: &Path) -> Option<StubSlice> {
311 let content = match std::fs::read_to_string(path) {
312 Ok(c) => c,
313 Err(e) => {
314 eprintln!("mir: cannot read stub file {}: {}", path.display(), e);
315 return None;
316 }
317 };
318 Some(stub_slice_from_source(
319 path.to_string_lossy().as_ref(),
320 &content,
321 None,
322 ))
323}
324
325fn collect_stub_dir_paths(dir: &Path, paths: &mut Vec<PathBuf>) {
326 let entries = match std::fs::read_dir(dir) {
327 Ok(e) => e,
328 Err(e) => {
329 eprintln!("mir: cannot read stub directory {}: {}", dir.display(), e);
330 return;
331 }
332 };
333 let mut dir_entries: Vec<PathBuf> = entries.filter_map(|e| e.ok().map(|e| e.path())).collect();
334 dir_entries.sort_unstable();
335 for path in dir_entries {
336 if path.is_dir() {
337 collect_stub_dir_paths(&path, paths);
338 } else if path.extension().is_some_and(|e| e == "php") {
339 paths.push(path);
340 }
341 }
342}
343
344#[allow(dead_code)]
349pub(crate) fn load_user_stubs(db: &mut MirDb, files: &[PathBuf], dirs: &[PathBuf]) {
350 for slice in user_stub_slices(files, dirs) {
351 db.ingest_stub_slice(&slice);
352 }
353}
354
355pub struct StubVfs {
376 files: HashMap<&'static str, &'static str>,
377}
378
379impl StubVfs {
380 pub fn new() -> Self {
382 let files = STUB_FILES.iter().map(|&(p, c)| (p, c)).collect();
383 Self { files }
384 }
385
386 pub fn get(&self, path: &str) -> Option<&'static str> {
388 self.files.get(path).copied()
389 }
390
391 pub fn is_stub_file(&self, path: &str) -> bool {
393 self.files.contains_key(path)
394 }
395}
396
397impl Default for StubVfs {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403pub fn is_builtin_function(name: &str) -> bool {
421 if !BUILTIN_FN_NAMES.is_empty() {
422 return BUILTIN_FN_NAMES.binary_search(&name).is_ok();
423 }
424 static FALLBACK: LazyLock<HashSet<Arc<str>>> = LazyLock::new(|| {
425 builtin_stub_slices_for_version(PhpVersion::LATEST)
426 .into_iter()
427 .flat_map(|slice| slice.functions.into_iter().map(|func| func.fqn))
428 .collect()
429 });
430 FALLBACK.contains(name)
431}
432
433#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::db::{
441 constant_exists_via_db, function_exists_via_db, type_exists_via_db, MirDatabase, MirDb,
442 };
443
444 fn stubs_codebase() -> MirDb {
445 let mut db = MirDb::default();
446 load_stubs(&mut db);
447 db
448 }
449
450 fn stubs_codebase_for(version: PhpVersion) -> MirDb {
451 let mut db = MirDb::default();
452 load_stubs_for_version(&mut db, version);
453 db
454 }
455
456 fn stub_function_for(version: PhpVersion, name: &str) -> Option<mir_codebase::FunctionStorage> {
457 builtin_stub_slices_for_version(version)
458 .into_iter()
459 .flat_map(|slice| slice.functions.into_iter())
460 .find(|func| func.fqn.as_ref() == name)
461 }
462
463 fn stub_class_for(version: PhpVersion, name: &str) -> Option<mir_codebase::ClassStorage> {
464 builtin_stub_slices_for_version(version)
465 .into_iter()
466 .flat_map(|slice| slice.classes.into_iter())
467 .find(|cls| cls.fqcn.as_ref() == name)
468 }
469
470 #[test]
471 fn since_tag_excludes_function_below_target() {
472 let cb = stubs_codebase_for(PhpVersion::new(7, 4));
474 assert!(
475 !function_exists_via_db(&cb, "str_contains"),
476 "str_contains should not be registered on PHP 7.4"
477 );
478 }
479
480 #[test]
481 fn since_tag_includes_function_at_target() {
482 let cb = stubs_codebase_for(PhpVersion::new(8, 0));
483 assert!(
484 function_exists_via_db(&cb, "str_contains"),
485 "str_contains should be registered on PHP 8.0"
486 );
487 }
488
489 #[test]
490 fn since_filter_applies_to_classes() {
491 let cb_old = stubs_codebase_for(PhpVersion::new(8, 1));
493 assert!(
494 !type_exists_via_db(&cb_old, "Random\\Randomizer"),
495 "Random\\Randomizer should not exist on PHP 8.1"
496 );
497 let cb_new = stubs_codebase_for(PhpVersion::new(8, 2));
498 assert!(
499 type_exists_via_db(&cb_new, "Random\\Randomizer"),
500 "Random\\Randomizer should exist on PHP 8.2"
501 );
502 }
503
504 #[test]
505 fn since_filter_applies_to_methods() {
506 let cls = stub_class_for(PhpVersion::new(7, 4), "DateTimeImmutable")
508 .expect("DateTimeImmutable must exist");
509 assert!(
510 !cls.own_methods.contains_key("createfrominterface"),
511 "createFromInterface should be absent on PHP 7.4"
512 );
513
514 let cls_new = stub_class_for(PhpVersion::new(8, 0), "DateTimeImmutable")
515 .expect("DateTimeImmutable must exist");
516 assert!(
517 cls_new.own_methods.contains_key("createfrominterface"),
518 "createFromInterface should be present on PHP 8.0"
519 );
520 }
521
522 #[test]
523 fn since_tag_excludes_constant_below_target() {
524 if STUB_FILES.is_empty() {
525 return;
526 }
527 let cb_old = stubs_codebase_for(PhpVersion::new(8, 0));
529 assert!(
530 !constant_exists_via_db(&cb_old, "IMAGETYPE_AVIF"),
531 "IMAGETYPE_AVIF should not be registered on PHP 8.0"
532 );
533 let cb_new = stubs_codebase_for(PhpVersion::new(8, 1));
534 assert!(
535 constant_exists_via_db(&cb_new, "IMAGETYPE_AVIF"),
536 "IMAGETYPE_AVIF should be registered on PHP 8.1"
537 );
538 }
539
540 #[test]
541 fn removed_tag_excludes_function_at_or_after_target() {
542 let cb = stubs_codebase_for(PhpVersion::new(8, 0));
544 assert!(
545 !function_exists_via_db(&cb, "each"),
546 "each should be removed on PHP 8.0"
547 );
548 let cb74 = stubs_codebase_for(PhpVersion::new(7, 4));
549 assert!(
550 function_exists_via_db(&cb74, "each"),
551 "each should still exist on PHP 7.4"
552 );
553 }
554
555 fn assert_fn(cb: &MirDb, name: &str) {
556 assert!(
557 function_exists_via_db(cb, name),
558 "expected stub for `{name}` to be registered"
559 );
560 }
561
562 #[test]
563 fn sscanf_vars_param_is_byref_and_variadic() {
564 let func = stub_function_for(PhpVersion::LATEST, "sscanf").expect("sscanf must be defined");
565 let vars = func.params.get(2).expect("sscanf must have a 3rd param");
566 assert!(vars.is_byref, "sscanf vars param must be by-ref");
567 assert!(vars.is_variadic, "sscanf vars param must be variadic");
568 }
569
570 #[test]
571 fn sscanf_output_vars_not_undefined() {
572 use crate::project::ProjectAnalyzer;
573 use mir_issues::IssueKind;
574
575 let src = "<?php\nfunction test($str) {\n sscanf($str, \"%d %d\", $row, $col);\n return $row + $col;\n}\n";
576 let tmp = std::env::temp_dir().join("mir_test_sscanf_undefined.php");
577 std::fs::write(&tmp, src).unwrap();
578 let result = ProjectAnalyzer::new().analyze(std::slice::from_ref(&tmp));
579 std::fs::remove_file(tmp).ok();
580 let undef: Vec<_> = result.issues.iter()
581 .filter(|i| matches!(&i.kind, IssueKind::UndefinedVariable { name } if name == "row" || name == "col"))
582 .collect();
583 assert!(
584 undef.is_empty(),
585 "sscanf output vars must not be reported as UndefinedVariable; got: {undef:?}"
586 );
587 }
588
589 #[test]
590 fn stream_functions_are_defined() {
591 let cb = stubs_codebase();
592 assert_fn(&cb, "stream_isatty");
593 assert_fn(&cb, "stream_select");
594 assert_fn(&cb, "stream_get_meta_data");
595 assert_fn(&cb, "stream_set_blocking");
596 assert_fn(&cb, "stream_copy_to_stream");
597 }
598
599 #[test]
600 fn preg_grep_is_defined() {
601 let cb = stubs_codebase();
602 assert_fn(&cb, "preg_grep");
603 }
604
605 #[test]
606 fn standard_missing_functions_are_defined() {
607 let cb = stubs_codebase();
608 assert_fn(&cb, "get_resource_type");
609 assert_fn(&cb, "ftruncate");
610 assert_fn(&cb, "umask");
611 assert_fn(&cb, "date_default_timezone_set");
612 assert_fn(&cb, "date_default_timezone_get");
613 }
614
615 #[test]
616 fn mb_missing_functions_are_defined() {
617 let cb = stubs_codebase();
618 assert_fn(&cb, "mb_strwidth");
619 assert_fn(&cb, "mb_convert_variables");
620 }
621
622 #[test]
623 fn pcntl_functions_are_defined() {
624 let cb = stubs_codebase();
625 assert_fn(&cb, "pcntl_signal");
626 assert_fn(&cb, "pcntl_async_signals");
627 assert_fn(&cb, "pcntl_signal_get_handler");
628 assert_fn(&cb, "pcntl_alarm");
629 }
630
631 #[test]
632 fn posix_functions_are_defined() {
633 let cb = stubs_codebase();
634 assert_fn(&cb, "posix_kill");
635 assert_fn(&cb, "posix_getpid");
636 }
637
638 #[test]
639 fn sapi_windows_functions_are_defined() {
640 let cb = stubs_codebase();
641 assert_fn(&cb, "sapi_windows_vt100_support");
642 assert_fn(&cb, "sapi_windows_cp_set");
643 assert_fn(&cb, "sapi_windows_cp_get");
644 assert_fn(&cb, "sapi_windows_cp_conv");
645 }
646
647 #[test]
648 fn cli_functions_are_defined() {
649 let cb = stubs_codebase();
650 assert_fn(&cb, "cli_set_process_title");
651 assert_fn(&cb, "cli_get_process_title");
652 }
653
654 #[test]
655 fn builtin_fn_names_has_sufficient_entries() {
656 if BUILTIN_FN_NAMES.is_empty() {
659 return;
660 }
661 assert!(
662 BUILTIN_FN_NAMES.len() >= 500,
663 "BUILTIN_FN_NAMES has only {} entries — \
664 build.rs may have failed to parse PhpStormStubsMap.php correctly",
665 BUILTIN_FN_NAMES.len()
666 );
667 }
668
669 #[test]
670 fn is_builtin_function_returns_true_for_known_builtins() {
671 assert!(is_builtin_function("strlen"), "strlen should be a builtin");
672 assert!(
673 is_builtin_function("array_map"),
674 "array_map should be a builtin"
675 );
676 assert!(
677 is_builtin_function("json_encode"),
678 "json_encode should be a builtin"
679 );
680 assert!(
681 is_builtin_function("preg_match"),
682 "preg_match should be a builtin"
683 );
684 }
685
686 #[test]
687 fn is_builtin_function_covers_stdlib_functions() {
688 assert!(is_builtin_function("bcadd"), "bcadd should be a builtin");
689 assert!(
690 is_builtin_function("sodium_crypto_secretbox"),
691 "sodium_crypto_secretbox should be a builtin"
692 );
693 }
694
695 #[test]
696 fn is_builtin_function_returns_false_for_unknown_names() {
697 assert!(
698 !is_builtin_function("my_custom_function"),
699 "my_custom_function should not be a builtin"
700 );
701 assert!(
702 !is_builtin_function(""),
703 "empty string should not be a builtin"
704 );
705 assert!(
706 !is_builtin_function("ast\\parse_file"),
707 "extension function should not be a builtin"
708 );
709 }
710
711 fn assert_cls(cb: &MirDb, name: &str) {
714 assert!(
715 type_exists_via_db(cb, name),
716 "expected stub class `{name}` to be registered"
717 );
718 }
719
720 fn assert_iface(cb: &MirDb, name: &str) {
721 assert!(
722 type_exists_via_db(cb, name),
723 "expected stub interface `{name}` to be registered"
724 );
725 }
726
727 fn assert_const(cb: &MirDb, name: &str) {
728 assert!(
729 constant_exists_via_db(cb, name),
730 "expected stub constant `{name}` to be registered"
731 );
732 }
733
734 #[test]
735 fn stubs_coverage_counts() {
736 let cb = stubs_codebase();
737 let fn_count = cb.function_count();
738 let type_count = cb.type_count();
739 let const_count = cb.constant_count();
740 assert!(fn_count > 500, "expected >500 functions, got {fn_count}");
742 assert!(type_count > 120, "expected >120 types, got {type_count}");
743 assert!(
744 const_count > 200,
745 "expected >200 constants, got {const_count}"
746 );
747 }
748
749 #[test]
750 fn curl_multi_exec_still_running_is_byref() {
751 let func = stub_function_for(PhpVersion::LATEST, "curl_multi_exec")
752 .expect("curl_multi_exec must be defined");
753 let still_running = func
754 .params
755 .iter()
756 .find(|p| p.name.as_ref() == "still_running")
757 .expect("curl_multi_exec must have a still_running param");
758 assert!(
759 still_running.is_byref,
760 "curl_multi_exec $still_running must be by-ref (generated from PHP stub)"
761 );
762 }
763
764 #[test]
767 fn stub_files_are_non_empty() {
768 assert!(
771 !STUB_FILES.is_empty(),
772 "STUB_FILES must not be empty — check build.rs find_workspace_root()"
773 );
774 }
775
776 #[test]
777 fn stub_vfs_resolves_all_paths() {
778 let vfs = StubVfs::new();
779 for &(path, expected_content) in STUB_FILES {
780 let got = vfs
781 .get(path)
782 .unwrap_or_else(|| panic!("StubVfs::get({path:?}) returned None"));
783 assert_eq!(got, expected_content, "StubVfs content mismatch for {path}");
784 assert!(
785 vfs.is_stub_file(path),
786 "StubVfs::is_stub_file({path:?}) returned false"
787 );
788 }
789 }
790
791 #[test]
792 fn stub_vfs_rejects_user_file_paths() {
793 let vfs = StubVfs::new();
794 assert!(!vfs.is_stub_file("/tmp/user_code.php"));
795 assert!(!vfs.is_stub_file("src/MyClass.php"));
796 assert!(!vfs.is_stub_file(""));
797 }
798
799 #[test]
800 fn symbol_to_file_paths_are_resolvable_via_stub_vfs() {
801 let mut db = MirDb::default();
802 load_stubs(&mut db);
803 let vfs = StubVfs::new();
804
805 for symbol in db.active_function_node_fqns() {
806 let Some(path) = db.symbol_defining_file(symbol.as_ref()) else {
807 continue;
808 };
809 assert!(
810 vfs.get(path.as_ref()).is_some(),
811 "symbol '{}' points to '{}' which StubVfs cannot resolve — \
812 go-to-definition would silently break for this symbol",
813 symbol,
814 path
815 );
816 }
817 }
818
819 #[test]
820 fn function_lookup_is_case_insensitive() {
821 let cb = stubs_codebase();
826 assert!(function_exists_via_db(&cb, "strlen"));
827 assert!(function_exists_via_db(&cb, "STRLEN"));
828 assert!(function_exists_via_db(&cb, "StrLen"));
829 assert!(function_exists_via_db(&cb, "Restore_Error_Handler"));
830 assert!(function_exists_via_db(&cb, "RESTORE_ERROR_HANDLER"));
831 }
832
833 #[test]
834 fn class_lookup_is_case_insensitive() {
835 let cb = stubs_codebase();
839 assert!(type_exists_via_db(&cb, "ArrayObject"));
840 assert!(type_exists_via_db(&cb, "arrayobject"));
841 assert!(type_exists_via_db(&cb, "ARRAYOBJECT"));
842 assert!(type_exists_via_db(&cb, "ArrayOBJECT"));
843 }
844
845 #[test]
846 fn constant_lookup_stays_case_sensitive() {
847 let cb = stubs_codebase();
851 assert!(constant_exists_via_db(&cb, "PHP_INT_MAX"));
852 assert!(!constant_exists_via_db(&cb, "php_int_max"));
853 assert!(!constant_exists_via_db(&cb, "Php_Int_Max"));
854 }
855
856 #[test]
857 fn stdlib_symbols_are_loaded() {
858 let cb = stubs_codebase();
859
860 assert_fn(&cb, "bcadd");
861 assert_fn(&cb, "bcsub");
862 assert_fn(&cb, "bcmul");
863 assert_fn(&cb, "bcdiv");
864 assert_fn(&cb, "sodium_crypto_secretbox");
865 assert_fn(&cb, "sodium_randombytes_buf");
866
867 assert_cls(&cb, "SplObjectStorage");
868 assert_cls(&cb, "SplHeap");
869 assert_cls(&cb, "IteratorIterator");
870 assert_cls(&cb, "FilterIterator");
871 assert_cls(&cb, "LimitIterator");
872 assert_cls(&cb, "CallbackFilterIterator");
873 assert_cls(&cb, "RegexIterator");
874 assert_cls(&cb, "AppendIterator");
875 assert_cls(&cb, "GlobIterator");
876 assert_cls(&cb, "ReflectionObject");
877 assert_cls(&cb, "Attribute");
878
879 assert_iface(&cb, "SeekableIterator");
880 assert_iface(&cb, "SplObserver");
881 assert_iface(&cb, "SplSubject");
882
883 assert_const(&cb, "PHP_INT_MAX");
884 assert_const(&cb, "PHP_INT_MIN");
885 assert_const(&cb, "PHP_EOL");
886 assert_const(&cb, "SORT_REGULAR");
887 assert_const(&cb, "JSON_THROW_ON_ERROR");
888 assert_const(&cb, "FILTER_VALIDATE_EMAIL");
889 assert_const(&cb, "PREG_OFFSET_CAPTURE");
890 assert_const(&cb, "M_PI");
891 assert_const(&cb, "PASSWORD_DEFAULT");
892 }
893}