Repository navigation
fix(checker): file-qualified diagnostics, autoload span identities and preludes, checker cost #1921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
nahime0
merged 8 commits into
illegalstudio:main
from
JGamboa:fix/checker-driver-cost-and-diagnostics
Oct 24, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
59f7661
fix(checker): file-qualified diagnostics, autoload span identities an…
JGamboa 36f728c
fix(diagnostics): name the entry file in include-resolution errors
JGamboa ddf8723
docs: describe file-qualified diagnostics, the autoload prelude probe…
JGamboa 6ab4729
docs: keep the autoload prelude probe note in its own pipeline entry
JGamboa 4666dae
Merge remote-tracking branch 'origin/main' into fix/checker-driver-co…
nahime0 db958b5
fix(autoload): keep probe global identities monotonic
nahime0 d28b4e0
fix(diagnostics): name entry files in early autoload warnings
nahime0 0c4d994
chore: merge main into pr-1921
nahime0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
|
|
||
| mod alias; | ||
| mod dev_packages; | ||
| pub mod prelude_probe; | ||
| mod index; | ||
| mod interpret; | ||
| mod registry; | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| //! Purpose: | ||
| //! Decides which detected standard-library preludes the AUTOLOAD CLOSURE of a program needs, | ||
| //! before those preludes are injected. | ||
| //! | ||
| //! Called from: | ||
| //! - `crate::pipeline::compile()`, right before the prelude injections. | ||
| //! | ||
| //! Key details: | ||
| //! - Each prelude (PDO, mysqli, tz, image, hash, curl, xml, var_export) is injected only when | ||
| //! the program uses its surface, and that decision has to be taken before name resolution so a | ||
| //! namespaced caller resolves to the injected declarations. The Composer/SPL autoload pass runs | ||
| //! AFTER name resolution, so a class file it loads that uses `PDO` or `CurlHandle` was never | ||
| //! scanned: the prelude was skipped and the program failed with `Unknown type: PDO`. | ||
| //! - The probe runs name resolution and the autoload pass on a COPY of the program, scans the | ||
| //! result with each prelude's own detector, and hands back the groups found. The real pass then | ||
| //! injects them exactly as if the entry file had used them, so the outcome is the one | ||
| //! `--with-<crate>` gives, without forcing the whole surface past reachability pruning. | ||
| //! - The copy's work is discarded, restoring thread-local source IDs and anonymous-class | ||
| //! numbers so the real parse keeps those identities. Process-global synthetic spans and | ||
| //! include temporaries remain monotonic, preserving identities other threads allocated. | ||
| //! - A probe failure is not reported here: the real pass hits the same error and reports it. | ||
|
|
||
| use std::collections::HashSet; | ||
| use std::path::Path; | ||
|
|
||
| use crate::parser::ast::{Program, Stmt}; | ||
| use crate::span::Span; | ||
|
|
||
| use super::Registry; | ||
|
|
||
| #[cfg(test)] | ||
| #[path = "prelude_probe_tests.rs"] | ||
| mod tests; | ||
|
|
||
| /// Every prelude group the probe can report, with the detector that decides it. | ||
| const PROBED_GROUPS: &[(&str, fn(&[Stmt]) -> bool)] = &[ | ||
| ("pdo", crate::pdo_prelude::program_uses_pdo), | ||
| ("mysqli", crate::mysqli_prelude::program_uses_mysqli), | ||
| ("tz", crate::tz_prelude::program_uses_surface), | ||
| ("image", crate::image_prelude::program_uses_surface), | ||
| ("hash", crate::hash_prelude::program_uses_surface), | ||
| ("curl", crate::curl_prelude::program_uses_surface), | ||
| ("xml", crate::xml_prelude::program_uses_surface), | ||
| ("var_export", crate::var_export_prelude::program_uses_surface), | ||
| ]; | ||
|
|
||
| /// Returns the prelude groups some file of the autoload closure uses. | ||
| /// | ||
| /// `program` is the include-resolved entry program, before any prelude is injected. Returns an | ||
| /// empty set when nothing is autoloadable or the probe cannot finish. | ||
| pub fn autoloaded_prelude_groups( | ||
| program: &Program, | ||
| base_dir: &Path, | ||
| registry: &Registry, | ||
| defines: &HashSet<String>, | ||
| ) -> HashSet<&'static str> { | ||
| if registry.is_empty() { | ||
| return HashSet::new(); | ||
| } | ||
| let span_counters = Span::counters_checkpoint(); | ||
| let anonymous_classes = crate::parser::anonymous_class_counter(); | ||
| let closure = crate::name_resolver::resolve(program.clone()) | ||
| .ok() | ||
| .and_then(|resolved| { | ||
| super::run_collecting_included_with_defines(resolved, base_dir, registry, defines).ok() | ||
| }); | ||
| Span::restore_counters(span_counters); | ||
| crate::parser::restore_anonymous_class_counter(anonymous_classes); | ||
| let Some((closure, _)) = closure else { | ||
| return HashSet::new(); | ||
| }; | ||
| PROBED_GROUPS | ||
| .iter() | ||
| .filter(|(_, detect)| detect(&closure)) | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| .map(|(group, _)| *group) | ||
| .collect() | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| //! Purpose: | ||
| //! Protects the identity allocation boundary between discarded probes and real autoloads. | ||
| //! | ||
| //! Called from: | ||
| //! - `autoload::prelude_probe` unit tests. | ||
| //! | ||
| //! Key details: | ||
| //! - Source and anonymous-class IDs are restored, while global include temporaries stay fresh. | ||
| //! - The fixture removes its files even when an assertion fails. | ||
|
|
||
| use std::fs; | ||
| use std::path::PathBuf; | ||
|
|
||
| use crate::parser::ast::{Program, StmtKind}; | ||
| use crate::span::Span; | ||
|
|
||
| use super::{autoloaded_prelude_groups, Registry}; | ||
|
|
||
| /// Owns the small Composer project used to exercise a real probe and autoload pass. | ||
| struct ProbeProject(PathBuf); | ||
|
|
||
| impl ProbeProject { | ||
| /// Creates an autoloaded class with a value include and an anonymous class. | ||
| fn new() -> Self { | ||
| let path = std::env::temp_dir().join(format!( | ||
| "elephc_probe_counter_{}_{:?}", std::process::id(), std::thread::current().id(), | ||
| )); | ||
| fs::create_dir(&path).unwrap(); | ||
| let project = Self(path); | ||
| fs::create_dir(project.0.join("src")).unwrap(); | ||
| fs::write(project.0.join("composer.json"), | ||
| r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#).unwrap(); | ||
| fs::write(project.0.join("src/value.php"), "<?php return 7;").unwrap(); | ||
| fs::write(project.0.join("src/Probe.php"), r#"<?php | ||
| namespace App; | ||
| $value = require __DIR__ . '/value.php'; | ||
| $object = new class {}; | ||
| class Probe { public ?\PDO $pdo = null; } | ||
| "#).unwrap(); | ||
| project | ||
| } | ||
| } | ||
|
|
||
| impl Drop for ProbeProject { | ||
| /// Cleans up the fixture on both success and unwinding. | ||
| fn drop(&mut self) { | ||
| fs::remove_dir_all(&self.0).unwrap(); | ||
| } | ||
| } | ||
|
|
||
| /// Parses the entry or seed program without allocating a physical-file identity. | ||
| fn parse(source: &str) -> Program { | ||
| crate::parser::parse(&crate::lexer::tokenize(source).unwrap()).unwrap() | ||
| } | ||
|
|
||
| /// Reads the resolver's hidden include temporary from the flattened top-level statements. | ||
| fn include_temporary_id(program: &[crate::parser::ast::Stmt]) -> Option<usize> { | ||
| program.iter().find_map(|stmt| match &stmt.kind { | ||
| StmtKind::Assign { name, .. } => name.strip_prefix("__elephc_inc_")?.parse().ok(), | ||
| StmtKind::NamespaceBlock { body, .. } | StmtKind::Synthetic(body) => { | ||
| include_temporary_id(body) | ||
| } | ||
| _ => None, | ||
| }) | ||
| } | ||
|
|
||
| /// A probe preserves real-file and anonymous-class naming without recycling its include temporary. | ||
| #[test] | ||
| fn probe_restores_local_identities_but_keeps_include_temporaries_monotonic() { | ||
| let project = ProbeProject::new(); | ||
| let defines = std::collections::HashSet::new(); | ||
| let seed = crate::resolver::resolve_collecting_includes_continuing( | ||
| parse("<?php $seed = require 'src/value.php';"), &project.0, &defines, | ||
| ).unwrap().0; | ||
| let previous_temporary = include_temporary_id(&seed).expect("seed include temporary"); | ||
| let (registry, program) = Registry::build(&project.0, parse("<?php new App\\Probe();")); | ||
| assert!(!registry.is_empty()); | ||
| let anonymous_class = crate::parser::anonymous_class_counter(); | ||
| let checkpoint = Span::counters_checkpoint(); | ||
| let source_id = Span::fresh_source_id(); | ||
| let synthetic_source_id = Span::fresh_synthetic_source_id(); | ||
| Span::restore_counters(checkpoint); | ||
|
|
||
| let groups = autoloaded_prelude_groups(&program, &project.0, ®istry, &defines); | ||
|
|
||
| assert!(groups.contains("pdo")); | ||
| assert_eq!(crate::parser::anonymous_class_counter(), anonymous_class); | ||
| assert_eq!(Span::fresh_source_id(), source_id); | ||
| assert_eq!(Span::fresh_synthetic_source_id(), synthetic_source_id); | ||
| let resolved = crate::name_resolver::resolve(program).unwrap(); | ||
| let (loaded, _) = crate::autoload::run_collecting_included_with_defines( | ||
| resolved, &project.0, ®istry, &defines, | ||
| ).unwrap(); | ||
| let actual_temporary = include_temporary_id(&loaded).expect("real autoload include temporary"); | ||
| assert!(actual_temporary > previous_temporary + 1, | ||
| "the probe's discarded include temporary must not be reused"); | ||
| let expected_anonymous = format!("class@anonymous#{anonymous_class}"); | ||
| assert!(loaded.iter().any(|stmt| matches!(&stmt.kind, | ||
| StmtKind::ClassDecl { name, .. } if name.ends_with(&expected_anonymous))), | ||
| "the real parse must retain its anonymous-class name"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.