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 (mut slice, _) = collector.collect_slice(&result.program);
307 mir_codebase::storage::deduplicate_params_in_slice(&mut slice);
308 slice
309}
310
311fn parse_stub_file_slice(path: &Path) -> Option<StubSlice> {
312 let content = match std::fs::read_to_string(path) {
313 Ok(c) => c,
314 Err(e) => {
315 eprintln!("mir: cannot read stub file {}: {}", path.display(), e);
316 return None;
317 }
318 };
319 Some(stub_slice_from_source(
320 path.to_string_lossy().as_ref(),
321 &content,
322 None,
323 ))
324}
325
326fn collect_stub_dir_paths(dir: &Path, paths: &mut Vec<PathBuf>) {
327 let entries = match std::fs::read_dir(dir) {
328 Ok(e) => e,
329 Err(e) => {
330 eprintln!("mir: cannot read stub directory {}: {}", dir.display(), e);
331 return;
332 }
333 };
334 let mut dir_entries: Vec<PathBuf> = entries.filter_map(|e| e.ok().map(|e| e.path())).collect();
335 dir_entries.sort_unstable();
336 for path in dir_entries {
337 if path.is_dir() {
338 collect_stub_dir_paths(&path, paths);
339 } else if path.extension().is_some_and(|e| e == "php") {
340 paths.push(path);
341 }
342 }
343}
344
345#[allow(dead_code)]
350pub(crate) fn load_user_stubs(db: &mut MirDb, files: &[PathBuf], dirs: &[PathBuf]) {
351 for slice in user_stub_slices(files, dirs) {
352 db.ingest_stub_slice(&slice);
353 }
354}
355
356pub struct StubVfs {
377 files: HashMap<&'static str, &'static str>,
378}
379
380impl StubVfs {
381 pub fn new() -> Self {
383 let files = STUB_FILES.iter().map(|&(p, c)| (p, c)).collect();
384 Self { files }
385 }
386
387 pub fn get(&self, path: &str) -> Option<&'static str> {
389 self.files.get(path).copied()
390 }
391
392 pub fn is_stub_file(&self, path: &str) -> bool {
394 self.files.contains_key(path)
395 }
396}
397
398impl Default for StubVfs {
399 fn default() -> Self {
400 Self::new()
401 }
402}
403
404pub fn is_builtin_function(name: &str) -> bool {
422 if !BUILTIN_FN_NAMES.is_empty() {
423 return BUILTIN_FN_NAMES.binary_search(&name).is_ok();
424 }
425 static FALLBACK: LazyLock<HashSet<Arc<str>>> = LazyLock::new(|| {
426 builtin_stub_slices_for_version(PhpVersion::LATEST)
427 .into_iter()
428 .flat_map(|slice| slice.functions.into_iter().map(|func| func.fqn))
429 .collect()
430 });
431 FALLBACK.contains(name)
432}
433
434#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::db::{
442 constant_exists_via_db, function_exists_via_db, type_exists_via_db, MirDatabase, MirDb,
443 };
444
445 fn stubs_codebase() -> MirDb {
446 let mut db = MirDb::default();
447 load_stubs(&mut db);
448 db
449 }
450
451 fn stubs_codebase_for(version: PhpVersion) -> MirDb {
452 let mut db = MirDb::default();
453 load_stubs_for_version(&mut db, version);
454 db
455 }
456
457 fn stub_function_for(version: PhpVersion, name: &str) -> Option<mir_codebase::FunctionStorage> {
458 builtin_stub_slices_for_version(version)
459 .into_iter()
460 .flat_map(|slice| slice.functions.into_iter())
461 .find(|func| func.fqn.as_ref() == name)
462 }
463
464 fn stub_class_for(version: PhpVersion, name: &str) -> Option<mir_codebase::ClassStorage> {
465 builtin_stub_slices_for_version(version)
466 .into_iter()
467 .flat_map(|slice| slice.classes.into_iter())
468 .find(|cls| cls.fqcn.as_ref() == name)
469 }
470
471 #[test]
472 fn since_tag_excludes_function_below_target() {
473 let cb = stubs_codebase_for(PhpVersion::new(7, 4));
475 assert!(
476 !function_exists_via_db(&cb, "str_contains"),
477 "str_contains should not be registered on PHP 7.4"
478 );
479 }
480
481 #[test]
482 fn since_tag_includes_function_at_target() {
483 let cb = stubs_codebase_for(PhpVersion::new(8, 0));
484 assert!(
485 function_exists_via_db(&cb, "str_contains"),
486 "str_contains should be registered on PHP 8.0"
487 );
488 }
489
490 #[test]
491 fn since_filter_applies_to_classes() {
492 let cb_old = stubs_codebase_for(PhpVersion::new(8, 1));
494 assert!(
495 !type_exists_via_db(&cb_old, "Random\\Randomizer"),
496 "Random\\Randomizer should not exist on PHP 8.1"
497 );
498 let cb_new = stubs_codebase_for(PhpVersion::new(8, 2));
499 assert!(
500 type_exists_via_db(&cb_new, "Random\\Randomizer"),
501 "Random\\Randomizer should exist on PHP 8.2"
502 );
503 }
504
505 #[test]
506 fn since_filter_applies_to_methods() {
507 let cls = stub_class_for(PhpVersion::new(7, 4), "DateTimeImmutable")
509 .expect("DateTimeImmutable must exist");
510 assert!(
511 !cls.own_methods.contains_key("createfrominterface"),
512 "createFromInterface should be absent on PHP 7.4"
513 );
514
515 let cls_new = stub_class_for(PhpVersion::new(8, 0), "DateTimeImmutable")
516 .expect("DateTimeImmutable must exist");
517 assert!(
518 cls_new.own_methods.contains_key("createfrominterface"),
519 "createFromInterface should be present on PHP 8.0"
520 );
521 }
522
523 #[test]
524 fn since_tag_excludes_constant_below_target() {
525 if STUB_FILES.is_empty() {
526 return;
527 }
528 let cb_old = stubs_codebase_for(PhpVersion::new(8, 0));
530 assert!(
531 !constant_exists_via_db(&cb_old, "IMAGETYPE_AVIF"),
532 "IMAGETYPE_AVIF should not be registered on PHP 8.0"
533 );
534 let cb_new = stubs_codebase_for(PhpVersion::new(8, 1));
535 assert!(
536 constant_exists_via_db(&cb_new, "IMAGETYPE_AVIF"),
537 "IMAGETYPE_AVIF should be registered on PHP 8.1"
538 );
539 }
540
541 #[test]
542 fn removed_tag_excludes_function_at_or_after_target() {
543 let cb = stubs_codebase_for(PhpVersion::new(8, 0));
545 assert!(
546 !function_exists_via_db(&cb, "each"),
547 "each should be removed on PHP 8.0"
548 );
549 let cb74 = stubs_codebase_for(PhpVersion::new(7, 4));
550 assert!(
551 function_exists_via_db(&cb74, "each"),
552 "each should still exist on PHP 7.4"
553 );
554 }
555
556 fn assert_fn(cb: &MirDb, name: &str) {
557 assert!(
558 function_exists_via_db(cb, name),
559 "expected stub for `{name}` to be registered"
560 );
561 }
562
563 #[test]
564 fn sscanf_vars_param_is_byref_and_variadic() {
565 let func = stub_function_for(PhpVersion::LATEST, "sscanf").expect("sscanf must be defined");
566 let vars = func.params.get(2).expect("sscanf must have a 3rd param");
567 assert!(vars.is_byref, "sscanf vars param must be by-ref");
568 assert!(vars.is_variadic, "sscanf vars param must be variadic");
569 }
570
571 #[test]
572 fn sscanf_output_vars_not_undefined() {
573 use crate::project::ProjectAnalyzer;
574 use mir_issues::IssueKind;
575
576 let src = "<?php\nfunction test($str) {\n sscanf($str, \"%d %d\", $row, $col);\n return $row + $col;\n}\n";
577 let tmp = std::env::temp_dir().join("mir_test_sscanf_undefined.php");
578 std::fs::write(&tmp, src).unwrap();
579 let result = ProjectAnalyzer::new().analyze(std::slice::from_ref(&tmp));
580 std::fs::remove_file(tmp).ok();
581 let undef: Vec<_> = result.issues.iter()
582 .filter(|i| matches!(&i.kind, IssueKind::UndefinedVariable { name } if name == "row" || name == "col"))
583 .collect();
584 assert!(
585 undef.is_empty(),
586 "sscanf output vars must not be reported as UndefinedVariable; got: {undef:?}"
587 );
588 }
589
590 #[test]
591 fn stream_functions_are_defined() {
592 let cb = stubs_codebase();
593 assert_fn(&cb, "stream_isatty");
594 assert_fn(&cb, "stream_select");
595 assert_fn(&cb, "stream_get_meta_data");
596 assert_fn(&cb, "stream_set_blocking");
597 assert_fn(&cb, "stream_copy_to_stream");
598 }
599
600 #[test]
601 fn preg_grep_is_defined() {
602 let cb = stubs_codebase();
603 assert_fn(&cb, "preg_grep");
604 }
605
606 #[test]
607 fn standard_missing_functions_are_defined() {
608 let cb = stubs_codebase();
609 assert_fn(&cb, "get_resource_type");
610 assert_fn(&cb, "ftruncate");
611 assert_fn(&cb, "umask");
612 assert_fn(&cb, "date_default_timezone_set");
613 assert_fn(&cb, "date_default_timezone_get");
614 }
615
616 #[test]
617 fn mb_missing_functions_are_defined() {
618 let cb = stubs_codebase();
619 assert_fn(&cb, "mb_strwidth");
620 assert_fn(&cb, "mb_convert_variables");
621 }
622
623 #[test]
624 fn pcntl_functions_are_defined() {
625 let cb = stubs_codebase();
626 assert_fn(&cb, "pcntl_signal");
627 assert_fn(&cb, "pcntl_async_signals");
628 assert_fn(&cb, "pcntl_signal_get_handler");
629 assert_fn(&cb, "pcntl_alarm");
630 }
631
632 #[test]
633 fn posix_functions_are_defined() {
634 let cb = stubs_codebase();
635 assert_fn(&cb, "posix_kill");
636 assert_fn(&cb, "posix_getpid");
637 }
638
639 #[test]
640 fn sapi_windows_functions_are_defined() {
641 let cb = stubs_codebase();
642 assert_fn(&cb, "sapi_windows_vt100_support");
643 assert_fn(&cb, "sapi_windows_cp_set");
644 assert_fn(&cb, "sapi_windows_cp_get");
645 assert_fn(&cb, "sapi_windows_cp_conv");
646 }
647
648 #[test]
649 fn cli_functions_are_defined() {
650 let cb = stubs_codebase();
651 assert_fn(&cb, "cli_set_process_title");
652 assert_fn(&cb, "cli_get_process_title");
653 }
654
655 #[test]
656 fn builtin_fn_names_has_sufficient_entries() {
657 if BUILTIN_FN_NAMES.is_empty() {
660 return;
661 }
662 assert!(
663 BUILTIN_FN_NAMES.len() >= 500,
664 "BUILTIN_FN_NAMES has only {} entries — \
665 build.rs may have failed to parse PhpStormStubsMap.php correctly",
666 BUILTIN_FN_NAMES.len()
667 );
668 }
669
670 #[test]
671 fn is_builtin_function_returns_true_for_known_builtins() {
672 assert!(is_builtin_function("strlen"), "strlen should be a builtin");
673 assert!(
674 is_builtin_function("array_map"),
675 "array_map should be a builtin"
676 );
677 assert!(
678 is_builtin_function("json_encode"),
679 "json_encode should be a builtin"
680 );
681 assert!(
682 is_builtin_function("preg_match"),
683 "preg_match should be a builtin"
684 );
685 }
686
687 #[test]
688 fn is_builtin_function_covers_stdlib_functions() {
689 assert!(is_builtin_function("bcadd"), "bcadd should be a builtin");
690 assert!(
691 is_builtin_function("sodium_crypto_secretbox"),
692 "sodium_crypto_secretbox should be a builtin"
693 );
694 }
695
696 #[test]
697 fn is_builtin_function_returns_false_for_unknown_names() {
698 assert!(
699 !is_builtin_function("my_custom_function"),
700 "my_custom_function should not be a builtin"
701 );
702 assert!(
703 !is_builtin_function(""),
704 "empty string should not be a builtin"
705 );
706 assert!(
707 !is_builtin_function("ast\\parse_file"),
708 "extension function should not be a builtin"
709 );
710 }
711
712 fn assert_cls(cb: &MirDb, name: &str) {
715 assert!(
716 type_exists_via_db(cb, name),
717 "expected stub class `{name}` to be registered"
718 );
719 }
720
721 fn assert_iface(cb: &MirDb, name: &str) {
722 assert!(
723 type_exists_via_db(cb, name),
724 "expected stub interface `{name}` to be registered"
725 );
726 }
727
728 fn assert_const(cb: &MirDb, name: &str) {
729 assert!(
730 constant_exists_via_db(cb, name),
731 "expected stub constant `{name}` to be registered"
732 );
733 }
734
735 #[test]
736 fn stubs_coverage_counts() {
737 let cb = stubs_codebase();
738 let fn_count = cb.function_count();
739 let type_count = cb.type_count();
740 let const_count = cb.constant_count();
741 assert!(fn_count > 500, "expected >500 functions, got {fn_count}");
743 assert!(type_count > 120, "expected >120 types, got {type_count}");
744 assert!(
745 const_count > 200,
746 "expected >200 constants, got {const_count}"
747 );
748 }
749
750 #[test]
751 fn curl_multi_exec_still_running_is_byref() {
752 let func = stub_function_for(PhpVersion::LATEST, "curl_multi_exec")
753 .expect("curl_multi_exec must be defined");
754 let still_running = func
755 .params
756 .iter()
757 .find(|p| p.name.as_ref() == "still_running")
758 .expect("curl_multi_exec must have a still_running param");
759 assert!(
760 still_running.is_byref,
761 "curl_multi_exec $still_running must be by-ref (generated from PHP stub)"
762 );
763 }
764
765 #[test]
768 fn stub_files_are_non_empty() {
769 assert!(
772 !STUB_FILES.is_empty(),
773 "STUB_FILES must not be empty — check build.rs find_workspace_root()"
774 );
775 }
776
777 #[test]
778 fn stub_vfs_resolves_all_paths() {
779 let vfs = StubVfs::new();
780 for &(path, expected_content) in STUB_FILES {
781 let got = vfs
782 .get(path)
783 .unwrap_or_else(|| panic!("StubVfs::get({path:?}) returned None"));
784 assert_eq!(got, expected_content, "StubVfs content mismatch for {path}");
785 assert!(
786 vfs.is_stub_file(path),
787 "StubVfs::is_stub_file({path:?}) returned false"
788 );
789 }
790 }
791
792 #[test]
793 fn stub_vfs_rejects_user_file_paths() {
794 let vfs = StubVfs::new();
795 assert!(!vfs.is_stub_file("/tmp/user_code.php"));
796 assert!(!vfs.is_stub_file("src/MyClass.php"));
797 assert!(!vfs.is_stub_file(""));
798 }
799
800 #[test]
801 fn symbol_to_file_paths_are_resolvable_via_stub_vfs() {
802 let mut db = MirDb::default();
803 load_stubs(&mut db);
804 let vfs = StubVfs::new();
805
806 for symbol in db.active_function_node_fqns() {
807 let Some(path) = db.symbol_defining_file(symbol.as_ref()) else {
808 continue;
809 };
810 assert!(
811 vfs.get(path.as_ref()).is_some(),
812 "symbol '{}' points to '{}' which StubVfs cannot resolve — \
813 go-to-definition would silently break for this symbol",
814 symbol,
815 path
816 );
817 }
818 }
819
820 #[test]
821 fn function_lookup_is_case_insensitive() {
822 let cb = stubs_codebase();
827 assert!(function_exists_via_db(&cb, "strlen"));
828 assert!(function_exists_via_db(&cb, "STRLEN"));
829 assert!(function_exists_via_db(&cb, "StrLen"));
830 assert!(function_exists_via_db(&cb, "Restore_Error_Handler"));
831 assert!(function_exists_via_db(&cb, "RESTORE_ERROR_HANDLER"));
832 }
833
834 #[test]
835 fn class_lookup_is_case_insensitive() {
836 let cb = stubs_codebase();
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 assert!(type_exists_via_db(&cb, "ArrayOBJECT"));
844 }
845
846 #[test]
847 fn constant_lookup_stays_case_sensitive() {
848 let cb = stubs_codebase();
852 assert!(constant_exists_via_db(&cb, "PHP_INT_MAX"));
853 assert!(!constant_exists_via_db(&cb, "php_int_max"));
854 assert!(!constant_exists_via_db(&cb, "Php_Int_Max"));
855 }
856
857 #[test]
858 fn stdlib_symbols_are_loaded() {
859 let cb = stubs_codebase();
860
861 assert_fn(&cb, "bcadd");
862 assert_fn(&cb, "bcsub");
863 assert_fn(&cb, "bcmul");
864 assert_fn(&cb, "bcdiv");
865 assert_fn(&cb, "sodium_crypto_secretbox");
866 assert_fn(&cb, "sodium_randombytes_buf");
867
868 assert_cls(&cb, "SplObjectStorage");
869 assert_cls(&cb, "SplHeap");
870 assert_cls(&cb, "IteratorIterator");
871 assert_cls(&cb, "FilterIterator");
872 assert_cls(&cb, "LimitIterator");
873 assert_cls(&cb, "CallbackFilterIterator");
874 assert_cls(&cb, "RegexIterator");
875 assert_cls(&cb, "AppendIterator");
876 assert_cls(&cb, "GlobIterator");
877 assert_cls(&cb, "ReflectionObject");
878 assert_cls(&cb, "Attribute");
879
880 assert_iface(&cb, "SeekableIterator");
881 assert_iface(&cb, "SplObserver");
882 assert_iface(&cb, "SplSubject");
883
884 assert_const(&cb, "PHP_INT_MAX");
885 assert_const(&cb, "PHP_INT_MIN");
886 assert_const(&cb, "PHP_EOL");
887 assert_const(&cb, "SORT_REGULAR");
888 assert_const(&cb, "JSON_THROW_ON_ERROR");
889 assert_const(&cb, "FILTER_VALIDATE_EMAIL");
890 assert_const(&cb, "PREG_OFFSET_CAPTURE");
891 assert_const(&cb, "M_PI");
892 assert_const(&cb, "PASSWORD_DEFAULT");
893 }
894}