1use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
18use std::ops::ControlFlow;
19use std::path::{Path, PathBuf};
20use std::sync::{Arc, LazyLock};
21
22use mir_codebase::storage::StubSlice;
23use php_ast::owned::visitor::{walk_owned_expr, walk_owned_program, OwnedVisitor};
24use php_ast::owned::ExprKind;
25use php_lexer::TokenKind;
26use rayon::prelude::*;
27
28use crate::db::MirDbStorage;
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) fn stub_content_for_path(path: &str) -> Option<&'static str> {
42 STUB_FILES
43 .iter()
44 .find_map(|&(p, c)| if p == path { Some(c) } else { None })
45}
46
47pub(crate) fn stub_path_for_function(name: &str) -> Option<&'static str> {
51 let lower = name.to_ascii_lowercase();
52 STUB_FN_INDEX
53 .binary_search_by(|(k, _)| (*k).cmp(lower.as_str()))
54 .ok()
55 .map(|i| STUB_FN_INDEX[i].1)
56}
57
58pub fn stub_path_for_class(fqcn: &str) -> Option<&'static str> {
67 let trimmed = fqcn.strip_prefix('\\').unwrap_or(fqcn);
68 let lower = trimmed.to_ascii_lowercase();
69 STUB_CLASS_INDEX
70 .binary_search_by(|(k, _)| (*k).cmp(lower.as_str()))
71 .ok()
72 .map(|i| STUB_CLASS_INDEX[i].1)
73}
74
75pub(crate) fn stub_path_for_constant(name: &str) -> Option<&'static str> {
78 let trimmed = name.strip_prefix('\\').unwrap_or(name);
79 STUB_CONST_INDEX
80 .binary_search_by(|(k, _)| (*k).cmp(trimmed))
81 .ok()
82 .map(|i| STUB_CONST_INDEX[i].1)
83}
84
85pub(crate) fn collect_referenced_builtin_paths(source: &str) -> Vec<&'static str> {
95 use php_lexer::lex_all;
96
97 let mut tokens: HashSet<&str> = HashSet::default();
98 let (lexed, _errors) = lex_all(source);
99
100 let mut i = 0;
101 while i < lexed.len() {
102 let token = &lexed[i];
103 if token.kind == TokenKind::Identifier {
104 let start = token.span.start as usize;
105 let end = token.span.end as usize;
106 if let Some(mut text) = source.get(start..end) {
107 let mut j = i + 1;
109 while j + 1 < lexed.len()
110 && lexed[j].kind == TokenKind::Backslash
111 && lexed[j + 1].kind == TokenKind::Identifier
112 {
113 j += 2;
114 if let Some(part) = source.get(start..(lexed[j - 1].span.end as usize)) {
115 text = part;
116 }
117 }
118 tokens.insert(text);
119 i = j;
120 } else {
121 i += 1;
122 }
123 } else {
124 i += 1;
125 }
126 }
127
128 let mut paths: HashSet<&'static str> = HashSet::default();
129 for token in tokens {
130 if let Some(p) = stub_path_for_function(token) {
131 paths.insert(p);
132 }
133 if let Some(p) = stub_path_for_class(token) {
134 paths.insert(p);
135 }
136 if let Some(p) = stub_path_for_constant(token) {
137 paths.insert(p);
138 }
139 }
140 paths.into_iter().collect()
141}
142
143pub(crate) fn collect_referenced_builtin_paths_from_ast(
152 program: &php_ast::owned::Program,
153) -> Vec<&'static str> {
154 let mut visitor = BuiltinRefVisitor {
155 paths: HashSet::default(),
156 };
157 let _ = walk_owned_program(&mut visitor, program);
158 visitor.paths.into_iter().collect()
159}
160
161struct BuiltinRefVisitor {
163 paths: HashSet<&'static str>,
164}
165
166impl OwnedVisitor for BuiltinRefVisitor {
167 fn visit_expr(&mut self, expr: &php_ast::owned::Expr) -> ControlFlow<()> {
168 match &expr.kind {
169 ExprKind::FunctionCall(call) => {
170 if let ExprKind::Identifier(name) = &call.name.kind {
171 if let Some(p) = stub_path_for_function(name.as_ref()) {
172 self.paths.insert(p);
173 }
174 }
175 }
176 ExprKind::New(new_expr) => {
177 if let ExprKind::Identifier(name) = &new_expr.class.kind {
178 if let Some(p) = stub_path_for_class(name.as_ref()) {
179 self.paths.insert(p);
180 }
181 }
182 }
183 ExprKind::StaticMethodCall(call) => {
184 if let ExprKind::Identifier(name) = &call.class.kind {
185 if let Some(p) = stub_path_for_class(name.as_ref()) {
186 self.paths.insert(p);
187 }
188 }
189 }
190 ExprKind::ClassConstAccess(access) => {
191 if let ExprKind::Identifier(name) = &access.class.kind {
192 if let Some(p) = stub_path_for_class(name.as_ref()) {
193 self.paths.insert(p);
194 }
195 }
196 }
197 ExprKind::Identifier(name) => {
198 if let Some(p) = stub_path_for_constant(name.as_ref()) {
199 self.paths.insert(p);
200 }
201 }
202 _ => {}
203 }
204 walk_owned_expr(self, expr)
205 }
206}
207
208pub fn stub_files() -> &'static [(&'static str, &'static str)] {
216 STUB_FILES
217}
218
219#[allow(dead_code)]
227pub(crate) fn load_stubs(db: &mut MirDbStorage) {
228 load_stubs_for_version(db, PhpVersion::LATEST);
229}
230
231#[allow(dead_code)]
236pub(crate) fn load_stubs_for_version(db: &mut MirDbStorage, php_version: PhpVersion) {
237 db.set_php_version(Arc::from(php_version.to_string().as_str()));
240 for (filename, content) in STUB_FILES {
244 let arc_path: Arc<str> = Arc::from(*filename);
245 let arc_content: Arc<str> = Arc::from(*content);
246 db.upsert_source_file(arc_path, arc_content);
247 }
248 let resolver: Arc<dyn crate::ClassResolver> = Arc::new(crate::StubClassResolver);
249 db.set_resolver(Some(resolver));
250}
251
252pub(crate) fn builtin_stub_slices_for_version(php_version: PhpVersion) -> Vec<StubSlice> {
253 STUB_FILES
254 .par_iter()
255 .map(|(filename, content)| stub_slice_from_source(filename, content, Some(php_version)))
256 .collect()
257}
258
259pub(crate) fn user_stub_slices(files: &[PathBuf], dirs: &[PathBuf]) -> Vec<StubSlice> {
260 let mut all_paths: Vec<PathBuf> = files.to_vec();
261 for dir in dirs {
262 collect_stub_dir_paths(dir, &mut all_paths);
263 }
264
265 all_paths
266 .par_iter()
267 .filter_map(|path| parse_stub_file_slice(path))
268 .collect()
269}
270
271pub(crate) fn stub_slice_from_source(
272 filename: &str,
273 content: &str,
274 php_version: Option<PhpVersion>,
275) -> StubSlice {
276 let result = php_rs_parser::parse(content);
277 let file: Arc<str> = Arc::from(filename);
278 let collector =
279 crate::collector::DefinitionCollector::new_for_slice(file, content, &result.source_map);
280 let collector = match php_version {
281 Some(version) => collector.with_php_version(version),
282 None => collector,
283 };
284 let (mut slice, _) = collector.collect_slice(&result.program);
285 mir_codebase::storage::deduplicate_params_in_slice(&mut slice);
286 slice
287}
288
289fn parse_stub_file_slice(path: &Path) -> Option<StubSlice> {
290 let content = match std::fs::read_to_string(path) {
291 Ok(c) => c,
292 Err(e) => {
293 eprintln!("mir: cannot read stub file {}: {}", path.display(), e);
294 return None;
295 }
296 };
297 Some(stub_slice_from_source(
298 path.to_string_lossy().as_ref(),
299 &content,
300 None,
301 ))
302}
303
304pub(crate) fn collect_stub_dir_paths(dir: &Path, paths: &mut Vec<PathBuf>) {
305 let entries = match std::fs::read_dir(dir) {
306 Ok(e) => e,
307 Err(e) => {
308 eprintln!("mir: cannot read stub directory {}: {}", dir.display(), e);
309 return;
310 }
311 };
312 let mut dir_entries: Vec<PathBuf> = entries.filter_map(|e| e.ok().map(|e| e.path())).collect();
313 dir_entries.sort_unstable();
314 for path in dir_entries {
315 if path.is_dir() {
316 collect_stub_dir_paths(&path, paths);
317 } else if path.extension().is_some_and(|e| e == "php") {
318 paths.push(path);
319 }
320 }
321}
322
323#[allow(dead_code)]
328pub(crate) fn load_user_stubs(_db: &mut MirDbStorage, files: &[PathBuf], dirs: &[PathBuf]) {
329 let _ = user_stub_slices(files, dirs);
332}
333
334pub struct StubVfs {
355 files: HashMap<&'static str, &'static str>,
356}
357
358impl StubVfs {
359 pub fn new() -> Self {
361 let files = STUB_FILES.iter().map(|&(p, c)| (p, c)).collect();
362 Self { files }
363 }
364
365 pub fn get(&self, path: &str) -> Option<&'static str> {
367 self.files.get(path).copied()
368 }
369
370 pub fn is_stub_file(&self, path: &str) -> bool {
372 self.files.contains_key(path)
373 }
374}
375
376impl Default for StubVfs {
377 fn default() -> Self {
378 Self::new()
379 }
380}
381
382pub struct StubClassResolver;
397
398impl crate::ClassResolver for StubClassResolver {
399 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
400 stub_path_for_class(fqcn)
404 .or_else(|| stub_path_for_function(fqcn))
405 .or_else(|| stub_path_for_constant(fqcn))
406 .map(std::path::PathBuf::from)
407 }
408}
409
410pub struct ChainedClassResolver {
419 primary: Arc<dyn crate::ClassResolver>,
420 fallback: Arc<dyn crate::ClassResolver>,
421}
422
423impl ChainedClassResolver {
424 pub fn new(
425 primary: Arc<dyn crate::ClassResolver>,
426 fallback: Arc<dyn crate::ClassResolver>,
427 ) -> Self {
428 Self { primary, fallback }
429 }
430}
431
432impl crate::ClassResolver for ChainedClassResolver {
433 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
434 self.primary
435 .resolve(fqcn)
436 .or_else(|| self.fallback.resolve(fqcn))
437 }
438}
439
440pub fn is_builtin_function(name: &str) -> bool {
458 if !BUILTIN_FN_NAMES.is_empty() {
459 return BUILTIN_FN_NAMES.binary_search(&name).is_ok();
460 }
461 static FALLBACK: LazyLock<HashSet<Arc<str>>> = LazyLock::new(|| {
462 builtin_stub_slices_for_version(PhpVersion::LATEST)
463 .into_iter()
464 .flat_map(|slice| slice.functions.into_iter().map(|func| func.fqn.clone()))
465 .collect()
466 });
467 FALLBACK.contains(name)
468}
469
470#[cfg(test)]
475mod tests {
476 use super::*;
477 use crate::db::{class_exists, constant_exists, function_exists, MirDatabase, MirDbStorage};
478
479 fn stubs_codebase() -> MirDbStorage {
480 let mut db = MirDbStorage::default();
481 load_stubs(&mut db);
482 db
483 }
484
485 fn stubs_codebase_for(version: PhpVersion) -> MirDbStorage {
486 let mut db = MirDbStorage::default();
487 load_stubs_for_version(&mut db, version);
488 db
489 }
490
491 fn stub_function_for(
492 version: PhpVersion,
493 name: &str,
494 ) -> Option<std::sync::Arc<mir_codebase::FunctionDef>> {
495 builtin_stub_slices_for_version(version)
496 .into_iter()
497 .flat_map(|slice| slice.functions.into_iter())
498 .find(|func| func.fqn.as_ref() == name)
499 }
500
501 fn stub_class_for(
502 version: PhpVersion,
503 name: &str,
504 ) -> Option<std::sync::Arc<mir_codebase::ClassDef>> {
505 builtin_stub_slices_for_version(version)
506 .into_iter()
507 .flat_map(|slice| slice.classes.into_iter())
508 .find(|cls| cls.fqcn.as_ref() == name)
509 }
510
511 #[test]
512 fn since_filter_applies_to_methods() {
513 let cls = stub_class_for(PhpVersion::new(7, 4), "DateTimeImmutable")
516 .expect("DateTimeImmutable must exist");
517 assert!(
518 !cls.own_methods.contains_key("createfrominterface"),
519 "createFromInterface should be absent on PHP 7.4"
520 );
521
522 let cls_new = stub_class_for(PhpVersion::new(8, 0), "DateTimeImmutable")
523 .expect("DateTimeImmutable must exist");
524 assert!(
525 cls_new.own_methods.contains_key("createfrominterface"),
526 "createFromInterface should be present on PHP 8.0"
527 );
528 }
529
530 #[test]
531 fn since_tag_excludes_function_below_target() {
532 let cb = stubs_codebase_for(PhpVersion::new(7, 4));
533 assert!(
534 !function_exists(&cb, "str_contains"),
535 "str_contains (@since 8.0) must be absent on PHP 7.4"
536 );
537 assert!(
538 !function_exists(&cb, "str_starts_with"),
539 "str_starts_with (@since 8.0) must be absent on PHP 7.4"
540 );
541 assert!(
542 !function_exists(&cb, "array_is_list"),
543 "array_is_list (@since 8.1) must be absent on PHP 7.4"
544 );
545
546 let cb80 = stubs_codebase_for(PhpVersion::new(8, 0));
547 assert!(
548 function_exists(&cb80, "str_contains"),
549 "str_contains must be present on PHP 8.0"
550 );
551 assert!(
552 !function_exists(&cb80, "array_is_list"),
553 "array_is_list (@since 8.1) must be absent on PHP 8.0"
554 );
555
556 let cb81 = stubs_codebase_for(PhpVersion::new(8, 1));
557 assert!(
558 function_exists(&cb81, "array_is_list"),
559 "array_is_list must be present on PHP 8.1"
560 );
561 }
562
563 #[test]
564 fn removed_tag_excludes_function_at_or_after_target() {
565 let cb74 = stubs_codebase_for(PhpVersion::new(7, 4));
567 assert!(
568 function_exists(&cb74, "hebrevc"),
569 "hebrevc must be present on PHP 7.4"
570 );
571
572 let cb80 = stubs_codebase_for(PhpVersion::new(8, 0));
573 assert!(
574 !function_exists(&cb80, "hebrevc"),
575 "hebrevc (@removed 8.0) must be absent on PHP 8.0"
576 );
577 }
578
579 fn assert_fn(cb: &MirDbStorage, name: &str) {
580 assert!(
581 function_exists(cb, name),
582 "expected stub for `{name}` to be registered"
583 );
584 }
585
586 #[test]
587 fn sscanf_vars_param_is_byref_and_variadic() {
588 let func = stub_function_for(PhpVersion::LATEST, "sscanf").expect("sscanf must be defined");
589 let vars = func.params.get(2).expect("sscanf must have a 3rd param");
590 assert!(vars.is_byref, "sscanf vars param must be by-ref");
591 assert!(vars.is_variadic, "sscanf vars param must be variadic");
592 }
593
594 #[test]
595 fn sscanf_output_vars_not_undefined() {
596 use crate::batch::BatchOptions;
597 use crate::session::AnalysisSession;
598 use crate::PhpVersion;
599 use mir_issues::IssueKind;
600
601 let src = "<?php\nfunction test($str) {\n sscanf($str, \"%d %d\", $row, $col);\n return $row + $col;\n}\n";
602 let tmp = std::env::temp_dir().join("mir_test_sscanf_undefined.php");
603 std::fs::write(&tmp, src).unwrap();
604 let session = AnalysisSession::new(PhpVersion::LATEST);
605 let result = session.analyze_paths(std::slice::from_ref(&tmp), &BatchOptions::new());
606 std::fs::remove_file(tmp).ok();
607 let undef: Vec<_> = result.issues.iter()
608 .filter(|i| matches!(&i.kind, IssueKind::UndefinedVariable { name } if name == "row" || name == "col"))
609 .collect();
610 assert!(
611 undef.is_empty(),
612 "sscanf output vars must not be reported as UndefinedVariable; got: {undef:?}"
613 );
614 }
615
616 #[test]
617 fn stream_functions_are_defined() {
618 let cb = stubs_codebase();
619 assert_fn(&cb, "stream_isatty");
620 assert_fn(&cb, "stream_select");
621 assert_fn(&cb, "stream_get_meta_data");
622 assert_fn(&cb, "stream_set_blocking");
623 assert_fn(&cb, "stream_copy_to_stream");
624 }
625
626 #[test]
627 fn preg_grep_is_defined() {
628 let cb = stubs_codebase();
629 assert_fn(&cb, "preg_grep");
630 }
631
632 #[test]
633 fn standard_missing_functions_are_defined() {
634 let cb = stubs_codebase();
635 assert_fn(&cb, "get_resource_type");
636 assert_fn(&cb, "ftruncate");
637 assert_fn(&cb, "umask");
638 assert_fn(&cb, "date_default_timezone_set");
639 assert_fn(&cb, "date_default_timezone_get");
640 }
641
642 #[test]
643 fn mb_missing_functions_are_defined() {
644 let cb = stubs_codebase();
645 assert_fn(&cb, "mb_strwidth");
646 assert_fn(&cb, "mb_convert_variables");
647 }
648
649 #[test]
650 fn pcntl_functions_are_defined() {
651 let cb = stubs_codebase();
652 assert_fn(&cb, "pcntl_signal");
653 assert_fn(&cb, "pcntl_async_signals");
654 assert_fn(&cb, "pcntl_signal_get_handler");
655 assert_fn(&cb, "pcntl_alarm");
656 }
657
658 #[test]
659 fn posix_functions_are_defined() {
660 let cb = stubs_codebase();
661 assert_fn(&cb, "posix_kill");
662 assert_fn(&cb, "posix_getpid");
663 }
664
665 #[test]
666 fn sapi_windows_functions_are_defined() {
667 let cb = stubs_codebase();
668 assert_fn(&cb, "sapi_windows_vt100_support");
669 assert_fn(&cb, "sapi_windows_cp_set");
670 assert_fn(&cb, "sapi_windows_cp_get");
671 assert_fn(&cb, "sapi_windows_cp_conv");
672 }
673
674 #[test]
675 fn cli_functions_are_defined() {
676 let cb = stubs_codebase();
677 assert_fn(&cb, "cli_set_process_title");
678 assert_fn(&cb, "cli_get_process_title");
679 }
680
681 #[test]
682 fn builtin_fn_names_has_sufficient_entries() {
683 if BUILTIN_FN_NAMES.is_empty() {
686 return;
687 }
688 assert!(
689 BUILTIN_FN_NAMES.len() >= 500,
690 "BUILTIN_FN_NAMES has only {} entries — \
691 build.rs may have failed to parse PhpStormStubsMap.php correctly",
692 BUILTIN_FN_NAMES.len()
693 );
694 }
695
696 #[test]
697 fn preg_match_matches_param_is_byref_with_type() {
698 let func = stub_function_for(PhpVersion::LATEST, "preg_match")
699 .expect("preg_match should exist in stubs");
700 let matches_param = func
701 .params
702 .iter()
703 .find(|p| p.name.as_ref() == "matches")
704 .expect("preg_match should have a $matches param");
705 assert!(matches_param.is_byref, "$matches should be byref");
706 assert!(
707 matches_param.ty.is_some(),
708 "$matches should have a type annotation (string[] from PHPDoc)"
709 );
710 }
711
712 #[test]
713 fn is_builtin_function_returns_true_for_known_builtins() {
714 assert!(is_builtin_function("strlen"), "strlen should be a builtin");
715 assert!(
716 is_builtin_function("array_map"),
717 "array_map should be a builtin"
718 );
719 assert!(
720 is_builtin_function("json_encode"),
721 "json_encode should be a builtin"
722 );
723 assert!(
724 is_builtin_function("preg_match"),
725 "preg_match should be a builtin"
726 );
727 }
728
729 #[test]
730 fn is_builtin_function_covers_stdlib_functions() {
731 assert!(is_builtin_function("bcadd"), "bcadd should be a builtin");
732 assert!(
733 is_builtin_function("sodium_crypto_secretbox"),
734 "sodium_crypto_secretbox should be a builtin"
735 );
736 }
737
738 #[test]
739 fn is_builtin_function_returns_false_for_unknown_names() {
740 assert!(
741 !is_builtin_function("my_custom_function"),
742 "my_custom_function should not be a builtin"
743 );
744 assert!(
745 !is_builtin_function(""),
746 "empty string should not be a builtin"
747 );
748 assert!(
749 !is_builtin_function("ast\\parse_file"),
750 "extension function should not be a builtin"
751 );
752 }
753
754 fn assert_cls(cb: &MirDbStorage, name: &str) {
757 assert!(
758 class_exists(cb, name),
759 "expected stub class `{name}` to be registered"
760 );
761 }
762
763 fn assert_iface(cb: &MirDbStorage, name: &str) {
764 assert!(
765 class_exists(cb, name),
766 "expected stub interface `{name}` to be registered"
767 );
768 }
769
770 fn assert_const(cb: &MirDbStorage, name: &str) {
771 assert!(
772 constant_exists(cb, name),
773 "expected stub constant `{name}` to be registered"
774 );
775 }
776
777 #[test]
778 fn stubs_coverage_counts() {
779 let mut fn_count = 0usize;
780 let mut type_count = 0usize;
781 let mut const_count = 0usize;
782 for slice in builtin_stub_slices_for_version(PhpVersion::LATEST) {
783 fn_count += slice.functions.len();
784 type_count += slice.classes.len()
785 + slice.interfaces.len()
786 + slice.traits.len()
787 + slice.enums.len();
788 const_count += slice.constants.len();
789 }
790 assert!(fn_count > 500, "expected >500 functions, got {fn_count}");
791 assert!(type_count > 120, "expected >120 types, got {type_count}");
792 assert!(
793 const_count > 200,
794 "expected >200 constants, got {const_count}"
795 );
796 }
797
798 #[test]
799 fn curl_multi_exec_still_running_is_byref() {
800 let func = stub_function_for(PhpVersion::LATEST, "curl_multi_exec")
801 .expect("curl_multi_exec must be defined");
802 let still_running = func
803 .params
804 .iter()
805 .find(|p| p.name.as_ref() == "still_running")
806 .expect("curl_multi_exec must have a still_running param");
807 assert!(
808 still_running.is_byref,
809 "curl_multi_exec $still_running must be by-ref (generated from PHP stub)"
810 );
811 }
812
813 #[test]
816 fn stub_files_are_non_empty() {
817 assert!(
820 !STUB_FILES.is_empty(),
821 "STUB_FILES must not be empty — check build.rs find_workspace_root()"
822 );
823 }
824
825 #[test]
826 fn stub_vfs_resolves_all_paths() {
827 let vfs = StubVfs::new();
828 for &(path, expected_content) in STUB_FILES {
829 let got = vfs
830 .get(path)
831 .unwrap_or_else(|| panic!("StubVfs::get({path:?}) returned None"));
832 assert_eq!(got, expected_content, "StubVfs content mismatch for {path}");
833 assert!(
834 vfs.is_stub_file(path),
835 "StubVfs::is_stub_file({path:?}) returned false"
836 );
837 }
838 }
839
840 #[test]
841 fn stub_vfs_rejects_user_file_paths() {
842 let vfs = StubVfs::new();
843 assert!(!vfs.is_stub_file("/tmp/user_code.php"));
844 assert!(!vfs.is_stub_file("src/MyClass.php"));
845 assert!(!vfs.is_stub_file(""));
846 }
847
848 #[test]
849 fn symbol_to_file_paths_are_resolvable_via_stub_vfs() {
850 let mut db = MirDbStorage::default();
851 db.init_workspace_revision();
852 load_stubs(&mut db);
853 let vfs = StubVfs::new();
854
855 let functions = crate::db::workspace_functions(&db);
856 for symbol in functions.iter() {
857 let Some(path) = db.symbol_defining_file(symbol.as_ref()) else {
858 continue;
859 };
860 assert!(
861 vfs.get(path.as_ref()).is_some(),
862 "symbol '{}' points to '{}' which StubVfs cannot resolve — \
863 go-to-definition would silently break for this symbol",
864 symbol,
865 path
866 );
867 }
868 }
869
870 #[test]
871 fn function_lookup_is_case_insensitive() {
872 let cb = stubs_codebase();
877 assert!(function_exists(&cb, "strlen"));
878 assert!(function_exists(&cb, "STRLEN"));
879 assert!(function_exists(&cb, "StrLen"));
880 assert!(function_exists(&cb, "Restore_Error_Handler"));
881 assert!(function_exists(&cb, "RESTORE_ERROR_HANDLER"));
882 }
883
884 #[test]
885 fn class_lookup_is_case_insensitive() {
886 let cb = stubs_codebase();
890 assert!(class_exists(&cb, "ArrayObject"));
891 assert!(class_exists(&cb, "arrayobject"));
892 assert!(class_exists(&cb, "ARRAYOBJECT"));
893 assert!(class_exists(&cb, "ArrayOBJECT"));
894 }
895
896 #[test]
897 fn constant_lookup_stays_case_sensitive() {
898 let cb = stubs_codebase();
902 assert!(constant_exists(&cb, "PHP_INT_MAX"));
903 assert!(!constant_exists(&cb, "php_int_max"));
904 assert!(!constant_exists(&cb, "Php_Int_Max"));
905 }
906
907 #[test]
908 fn stdlib_symbols_are_loaded() {
909 let cb = stubs_codebase();
910
911 assert_fn(&cb, "bcadd");
912 assert_fn(&cb, "bcsub");
913 assert_fn(&cb, "bcmul");
914 assert_fn(&cb, "bcdiv");
915 assert_fn(&cb, "sodium_crypto_secretbox");
916 assert_fn(&cb, "sodium_randombytes_buf");
917
918 assert_cls(&cb, "SplObjectStorage");
919 assert_cls(&cb, "SplHeap");
920 assert_cls(&cb, "IteratorIterator");
921 assert_cls(&cb, "FilterIterator");
922 assert_cls(&cb, "LimitIterator");
923 assert_cls(&cb, "CallbackFilterIterator");
924 assert_cls(&cb, "RegexIterator");
925 assert_cls(&cb, "AppendIterator");
926 assert_cls(&cb, "GlobIterator");
927 assert_cls(&cb, "ReflectionObject");
928 assert_cls(&cb, "Attribute");
929
930 assert_iface(&cb, "SeekableIterator");
931 assert_iface(&cb, "SplObserver");
932 assert_iface(&cb, "SplSubject");
933
934 assert_const(&cb, "PHP_INT_MAX");
935 assert_const(&cb, "PHP_INT_MIN");
936 assert_const(&cb, "PHP_EOL");
937 assert_const(&cb, "SORT_REGULAR");
938 assert_const(&cb, "JSON_THROW_ON_ERROR");
939 assert_const(&cb, "FILTER_VALIDATE_EMAIL");
940 assert_const(&cb, "PREG_OFFSET_CAPTURE");
941 assert_const(&cb, "M_PI");
942 assert_const(&cb, "PASSWORD_DEFAULT");
943 }
944}