From 8923c35cc728af93f3637d1c05b08faf7d63eddf Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Fri, 16 Oct 2026 10:57:48 +0200 Subject: [PATCH 1/6] Rust: Rename `cfg` to `config` --- rust/extractor/src/main.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/rust/extractor/src/main.rs b/rust/extractor/src/main.rs index 72b6f20c9fb0..55e5934f853c 100644 --- a/rust/extractor/src/main.rs +++ b/rust/extractor/src/main.rs @@ -212,12 +212,12 @@ fn cwd() -> anyhow::Result { } fn main() -> anyhow::Result<()> { - let mut cfg = config::Config::extract().context("failed to load configuration")?; - if cfg.qltest { - qltest::prepare(&mut cfg)?; + let mut config = config::Config::extract().context("failed to load configuration")?; + if config.qltest { + qltest::prepare(&mut config)?; } let start = Instant::now(); - let (flame_layer, _flush_guard) = if let Some(path) = &cfg.logging_flamegraph { + let (flame_layer, _flush_guard) = if let Some(path) = &config.logging_flamegraph { tracing_flame::FlameLayer::with_file(path) .ok() .map(|(a, b)| (Some(a), Some(b))) @@ -229,18 +229,18 @@ fn main() -> anyhow::Result<()> { tracing_subscriber::registry() .with(codeql_extractor::extractor::default_subscriber_with_level( "single_arch", - &cfg.logging_verbosity, + &config.logging_verbosity, )) .with(flame_layer) .init(); - info!("{cfg:#?}\n"); + info!("{config:#?}\n"); - let traps = trap::TrapFileProvider::new(&cfg).context("failed to set up trap files")?; + let traps = trap::TrapFileProvider::new(&config).context("failed to set up trap files")?; let archiver = archive::Archiver { - root: cfg.source_archive_dir.clone(), + root: config.source_archive_dir.clone(), }; let mut extractor = Extractor::new(&archiver, &traps); - let files: Vec = cfg + let files = config .inputs .iter() .map(|file| { @@ -250,7 +250,7 @@ fn main() -> anyhow::Result<()> { // [1]: https://github.com/rust-lang/rust-analyzer/issues/18894#issuecomment-2580014730 dunce::canonicalize(&file).unwrap_or(file) }) - .collect(); + .collect::>(); let manifests = extractor.find_manifests(&files)?; let mut map: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests .iter() @@ -272,13 +272,13 @@ fn main() -> anyhow::Result<()> { } let cwd = cwd()?; log_project_toolchain(); - let (cargo_config, load_cargo_config) = cfg.to_cargo_config(&cwd); - let library_mode = if cfg.extract_dependencies_as_source { + let (cargo_config, load_cargo_config) = config.to_cargo_config(&cwd); + let library_mode = if config.extract_dependencies_as_source { SourceKind::Source } else { SourceKind::Library }; - let source_mode = if cfg.force_library_mode { + let source_mode = if config.force_library_mode { library_mode } else { SourceKind::Source @@ -335,5 +335,5 @@ fn main() -> anyhow::Result<()> { } } - extractor.emit_extraction_diagnostics(start, &cfg) + extractor.emit_extraction_diagnostics(start, &config) } From a5a3b48f2a85d792bc03c4d87123a6884a7dfb54 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Fri, 16 Oct 2026 11:42:11 +0200 Subject: [PATCH 2/6] Rust: Refactor `find_workspace` and add doc comment --- rust/extractor/src/rust_analyzer.rs | 38 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/rust/extractor/src/rust_analyzer.rs b/rust/extractor/src/rust_analyzer.rs index 25d88ec5ce6b..d3129af12796 100644 --- a/rust/extractor/src/rust_analyzer.rs +++ b/rust/extractor/src/rust_analyzer.rs @@ -217,6 +217,12 @@ fn workspace_members_match(workspace_dir: &AbsPath, members: &[String], target: }) } +/// Finds an enclosing Cargo workspace that includes `manifest`. That is, its +/// `members` patterns include the manifest's directory and its `exclude` +/// patterns do not. +/// +/// Returns `None` if the manifest itself defines a workspace, is not a Cargo +/// manifest, cannot be loaded, or has no matching workspace. fn find_workspace(reader: &mut TomlReader, manifest: &ProjectManifest) -> Option { let ProjectManifest::CargoToml(cargo) = manifest else { return None; @@ -224,12 +230,13 @@ fn find_workspace(reader: &mut TomlReader, manifest: &ProjectManifest) -> Option let parsed_cargo = reader.read(cargo).ok()?; if parsed_cargo.workspace.is_some() { debug!("{cargo} is a workspace"); - return Some(manifest.clone()); + return None; } let Some(parent_dir) = cargo.parent().parent() else { warn!("no parent dir for {cargo}"); return None; }; + // Search upward from `parent_dir` for manifest files. let discovered = ProjectManifest::discover(parent_dir) .map_err(|e| { error!( @@ -240,25 +247,22 @@ fn find_workspace(reader: &mut TomlReader, manifest: &ProjectManifest) -> Option }) .ok()?; discovered - .iter() - .find_map(|it| match it { - ProjectManifest::CargoToml(other) - if cargo.starts_with(other.parent()) - && reader.read(other).is_ok_and(|it| { - it.workspace.as_ref().is_some_and(|w| { - workspace_members_match(other.parent(), &w.members, cargo.parent()) - && !workspace_members_match( - other.parent(), - &w.exclude, - cargo.parent(), - ) - }) - }) => + .into_iter() + .find(|it| { + if let ProjectManifest::CargoToml(other) = it + && cargo.starts_with(other.parent()) + && reader.read(other).is_ok_and(|it| { + it.workspace.as_ref().is_some_and(|w| { + workspace_members_match(other.parent(), &w.members, cargo.parent()) + && !workspace_members_match(other.parent(), &w.exclude, cargo.parent()) + }) + }) { debug!("found workspace {other} containing {cargo}"); - Some(it.clone()) + true + } else { + false } - _ => None, }) .or_else(|| { debug!("no workspace found for {cargo}"); From 87754be930404fc77d52b1ddb8be04043339ebc2 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 09:11:20 +0200 Subject: [PATCH 3/6] Rust: Extract workspace grouping into a helper --- rust/extractor/src/main.rs | 54 ++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/rust/extractor/src/main.rs b/rust/extractor/src/main.rs index 55e5934f853c..af650ac5713b 100644 --- a/rust/extractor/src/main.rs +++ b/rust/extractor/src/main.rs @@ -200,6 +200,39 @@ impl<'a> Extractor<'a> { self.steps.push(ExtractionStep::find_manifests(before)); ret } + + fn group_files<'b>( + &mut self, + manifests: &'b [ProjectManifest], + files: &'b [PathBuf], + ) -> HashMap<&'b Path, (&'b ProjectManifest, Vec<&'b Path>)> { + let mut workspaces: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests + .iter() + .map(|manifest| { + ( + manifest.manifest_path().parent().as_ref(), + (manifest, Vec::new()), + ) + }) + .collect(); + + 'outer: for file in files { + for ancestor in file.as_path().ancestors() { + if let Some((_, files)) = workspaces.get_mut(ancestor) { + files.push(file); + continue 'outer; + } + } + self.extract_without_semantics( + file, + SourceKind::Source, + RustAnalyzerNoSemantics::warning("no manifest found"), + ); + } + + workspaces.retain(|_, (_, files)| !files.is_empty()); + workspaces + } } fn cwd() -> anyhow::Result { @@ -252,24 +285,7 @@ fn main() -> anyhow::Result<()> { }) .collect::>(); let manifests = extractor.find_manifests(&files)?; - let mut map: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests - .iter() - .map(|x| (x.manifest_path().parent().as_ref(), (x, Vec::new()))) - .collect(); - - 'outer: for file in &files { - for ancestor in file.as_path().ancestors() { - if let Some((_, files)) = map.get_mut(ancestor) { - files.push(file); - continue 'outer; - } - } - extractor.extract_without_semantics( - file, - SourceKind::Source, - RustAnalyzerNoSemantics::warning("no manifest found"), - ); - } + let workspaces = extractor.group_files(&manifests, &files); let cwd = cwd()?; log_project_toolchain(); let (cargo_config, load_cargo_config) = config.to_cargo_config(&cwd); @@ -285,7 +301,7 @@ fn main() -> anyhow::Result<()> { }; let mut processed_files: HashSet = HashSet::from_iter(files.iter().cloned()); - for (manifest, files) in map.values().filter(|(_, files)| !files.is_empty()) { + for (manifest, files) in workspaces.values() { if let Some((ref db, ref vfs)) = extractor.load_manifest(manifest, &cargo_config, &load_cargo_config) { From c59c72cea6a16ffdebce41b2c02c7628819e05cf Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 10:36:46 +0200 Subject: [PATCH 4/6] Rust: Select dependency extraction contexts by feature configuration Defer dependency extraction until all workspaces have been considered, prefer configurations enabling more Cargo features, and use deterministic tie-breaking. --- rust/extractor/src/configuration.rs | 145 ++++++++++++++++++++++++++++ rust/extractor/src/diagnostics.rs | 29 +++++- rust/extractor/src/main.rs | 66 ++++++------- rust/extractor/src/util.rs | 15 +++ 4 files changed, 218 insertions(+), 37 deletions(-) create mode 100644 rust/extractor/src/configuration.rs create mode 100644 rust/extractor/src/util.rs diff --git a/rust/extractor/src/configuration.rs b/rust/extractor/src/configuration.rs new file mode 100644 index 000000000000..b1bf13a8db3c --- /dev/null +++ b/rust/extractor/src/configuration.rs @@ -0,0 +1,145 @@ +use crate::util::cmp_option_by; +use itertools::Itertools; +use ra_ap_base_db::SourceDatabase; +use ra_ap_cfg::{CfgAtom, CfgOptions}; +use ra_ap_hir::Semantics; +use ra_ap_ide_db::RootDatabase; +use ra_ap_project_model::ProjectManifest; +use ra_ap_vfs::{FileId, VfsPath}; +use std::cmp::Ordering; +use std::collections::{BTreeMap, HashMap, HashSet, btree_map::Entry}; +use std::path::{Path, PathBuf}; + +/// Returns the enclosing directory of the manifest file. +pub fn manifest_parent(manifest: &ProjectManifest) -> &Path { + manifest.manifest_path().parent().as_ref() +} + +/// Compare two `CfgOptions`. Feature count wins, sorted cfg options break ties. +fn compare_cfg_options(left: &CfgOptions, right: &CfgOptions) -> Ordering { + let feature_count = |cfg_options: &CfgOptions| { + cfg_options + .into_iter() + .filter(|cfg| matches!(cfg, CfgAtom::KeyValue { key, .. } if key.as_str() == "feature")) + .count() + }; + feature_count(left) + .cmp(&feature_count(right)) + .then_with(|| left.into_iter().sorted().cmp(right.into_iter().sorted())) +} + +#[derive(PartialEq, Eq)] +struct DependencySource<'a> { + cfg_options: Option, + manifest: &'a ProjectManifest, +} + +impl Ord for DependencySource<'_> { + fn cmp(&self, other: &Self) -> Ordering { + // Compare by cfg options first, then by manifest parent directory + // (arbitrary but deterministic). + cmp_option_by(&self.cfg_options, &other.cfg_options, compare_cfg_options) + .then_with(|| manifest_parent(self.manifest).cmp(manifest_parent(other.manifest))) + } +} + +impl PartialOrd for DependencySource<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Map dependency file paths to their highest-priority workspace context. +#[derive(Default)] +pub struct DependencyFiles<'a> { + files: BTreeMap>, +} + +impl<'a> DependencyFiles<'a> { + /// Considers this workspace as an extraction context for a dependency file, + /// retaining only the highest-priority context seen so far. The lexicographically + /// largest workspace path breaks identical-priority ties. + pub fn add_file( + &mut self, + manifest: &'a ProjectManifest, + file_id: FileId, + file: &VfsPath, + semantics: &Semantics<'_, RootDatabase>, + source_files: &HashSet, + ) { + let db = semantics.db; + if let Some(file) = file.as_path().map(<_ as AsRef>::as_ref) + && file.extension().is_some_and(|ext| ext == "rs") + && !source_files.contains(file) + && let root = db.file_source_root(file_id).source_root_id(db) + && db.source_root(root).source_root(db).is_library + { + let cfg_options = semantics + .file_to_module_def(file_id) + .map(|module| module.krate(db).cfg(db).clone()); + self.add_candidate(file, cfg_options, manifest); + } + } + + fn add_candidate( + &mut self, + file: &Path, + cfg_options: Option, + manifest: &'a ProjectManifest, + ) { + let candidate = DependencySource { + cfg_options, + manifest, + }; + match self.files.entry(file.to_owned()) { + Entry::Occupied(mut entry) => { + if candidate > *entry.get() { + entry.insert(candidate); + } + } + Entry::Vacant(entry) => { + entry.insert(candidate); + } + } + } + + /// Groups the selected dependency files by manifest for the dependency extraction pass. + pub fn by_manifests(self) -> HashMap<&'a ProjectManifest, Vec> { + self.files + .into_iter() + .map(|(file, source)| (source.manifest, file)) + .into_group_map() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ra_ap_intern::Symbol; + + #[test] + fn selects_highest_priority_in_either_order() { + let cfg = |features: &[&str]| { + let mut cfg = CfgOptions::default(); + for feature in features { + cfg.insert_key_value(Symbol::intern("feature"), Symbol::intern(feature)); + } + cfg + }; + let root = crate::cwd().unwrap(); + let manifest = |path| ProjectManifest::from_manifest_file(root.join(path)).unwrap(); + let candidates = [ + (manifest("z/Cargo.toml"), cfg(&["z"])), + (manifest("a/Cargo.toml"), cfg(&["a", "b"])), + (manifest("b/Cargo.toml"), cfg(&["a", "b"])), + ]; + for order in [[0, 1, 2], [2, 1, 0]] { + let mut plan = DependencyFiles::default(); + for (manifest, cfg) in order.map(|index| &candidates[index]) { + plan.add_candidate(Path::new("lib.rs"), Some(cfg.clone()), manifest); + } + let expected = HashMap::from([(&candidates[2].0, vec![PathBuf::from("lib.rs")])]); + assert_eq!(plan.by_manifests(), expected); + } + } +} diff --git a/rust/extractor/src/diagnostics.rs b/rust/extractor/src/diagnostics.rs index 0db3358d558a..541cef1309e8 100644 --- a/rust/extractor/src/diagnostics.rs +++ b/rust/extractor/src/diagnostics.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Utc}; use ra_ap_project_model::ProjectManifest; use serde::Serialize; use serde::ser::SerializeMap; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::fs::File; use std::path::{Path, PathBuf}; @@ -220,13 +220,15 @@ struct ExtractionSummary { type ExtractionDiagnostics = Diagnostics; fn summary(start: Instant, steps: &[ExtractionStep]) -> ExtractionSummary { - let mut number_of_manifests = 0; + // The manifests loaded during extraction. We might load the same manifest + // multiple times, so we use a set to keep track of unique manifests. + let mut manifests = HashSet::new(); let mut number_of_files = 0; let mut durations = HashMap::new(); for step in steps { match &step.action { ExtractionStepKind::LoadManifest => { - number_of_manifests += 1; + manifests.extend(step.file.as_ref()); } ExtractionStepKind::Parse => { number_of_files += 1; @@ -244,7 +246,7 @@ fn summary(start: Instant, steps: &[ExtractionStep]) -> ExtractionSummary { } info!("total duration: {total}"); ExtractionSummary { - number_of_manifests, + number_of_manifests: manifests.len(), number_of_files, durations: DurationsSummary { durations, total }, } @@ -284,3 +286,22 @@ pub fn emit_extraction_diagnostics( .with_context(|| format!("writing to diagnostics file {}", target.display()))?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counts_unique_manifests() { + let steps: Vec<_> = ["a/Cargo.toml", "b/Cargo.toml", "a/Cargo.toml"] + .into_iter() + .map(|path| ExtractionStep { + action: ExtractionStepKind::LoadManifest, + file: Some(PathBuf::from(path)), + ms: 1, + }) + .collect(); + let summary = summary(Instant::now(), &steps); + assert_eq!(summary.number_of_manifests, 2); + } +} diff --git a/rust/extractor/src/main.rs b/rust/extractor/src/main.rs index af650ac5713b..00d7b5e35820 100644 --- a/rust/extractor/src/main.rs +++ b/rust/extractor/src/main.rs @@ -5,7 +5,7 @@ use crate::translate::SourceKind; use crate::trap::TrapId; use anyhow::Context; use archive::Archiver; -use ra_ap_base_db::SourceDatabase; +use configuration::{DependencyFiles, manifest_parent}; use ra_ap_hir::Semantics; use ra_ap_ide_db::RootDatabase; use ra_ap_ide_db::line_index::{LineCol, LineIndex}; @@ -14,13 +14,9 @@ use ra_ap_paths::{AbsPathBuf, Utf8PathBuf}; use ra_ap_project_model::{CargoConfig, ProjectManifest}; use ra_ap_vfs::Vfs; use rust_analyzer::{ParseResult, RustAnalyzer}; -use std::collections::HashSet; -use std::hash::RandomState; +use std::collections::{BTreeMap, HashSet}; +use std::path::{Path, PathBuf}; use std::time::Instant; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; use std::{env, fs}; use tracing::{error, info, warn}; use tracing_subscriber::layer::SubscriberExt; @@ -28,6 +24,7 @@ use tracing_subscriber::util::SubscriberInitExt; mod archive; mod config; +mod configuration; mod crate_graph; mod diagnostics; pub mod generated; @@ -36,6 +33,7 @@ mod rust_analyzer; mod toolchain; mod translate; pub mod trap; +mod util; struct Extractor<'a> { archiver: &'a Archiver, @@ -205,15 +203,10 @@ impl<'a> Extractor<'a> { &mut self, manifests: &'b [ProjectManifest], files: &'b [PathBuf], - ) -> HashMap<&'b Path, (&'b ProjectManifest, Vec<&'b Path>)> { - let mut workspaces: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests + ) -> BTreeMap<&'b Path, (&'b ProjectManifest, Vec<&'b Path>)> { + let mut workspaces: BTreeMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests .iter() - .map(|manifest| { - ( - manifest.manifest_path().parent().as_ref(), - (manifest, Vec::new()), - ) - }) + .map(|manifest| (manifest_parent(manifest), (manifest, Vec::new()))) .collect(); 'outer: for file in files { @@ -299,36 +292,30 @@ fn main() -> anyhow::Result<()> { } else { SourceKind::Source }; - let mut processed_files: HashSet = - HashSet::from_iter(files.iter().cloned()); + let source_files = HashSet::from_iter(files.iter().cloned()); + + let mut dependencies = DependencyFiles::default(); for (manifest, files) in workspaces.values() { - if let Some((ref db, ref vfs)) = + if let Some((db, vfs)) = extractor.load_manifest(manifest, &cargo_config, &load_cargo_config) { let before_crate_graph = Instant::now(); - crate_graph::extract_crate_graph(extractor.traps, db, vfs); + crate_graph::extract_crate_graph(extractor.traps, &db, &vfs); extractor .steps .push(ExtractionStep::crate_graph(before_crate_graph)); - let semantics = Semantics::new(db); + let semantics = Semantics::new(&db); for file in files { - match extractor.load_source(file, &semantics, vfs) { - Ok(()) => extractor.extract_with_semantics(file, &semantics, vfs, source_mode), + match extractor.load_source(file, &semantics, &vfs) { + Ok(()) => extractor.extract_with_semantics(file, &semantics, &vfs, source_mode), Err(e) => extractor.extract_without_semantics(file, source_mode, e), }; } + // The same dependency path may appear in multiple workspaces' VFSs. + // We record it by updating its preferred context, but defer + // extraction until all workspaces have contributed their priority. for (file_id, file) in vfs.iter() { - if let Some(file) = file.as_path().map(<_ as AsRef>::as_ref) - && file.extension().is_some_and(|ext| ext == "rs") - && processed_files.insert(file.to_owned()) - && db - .source_root(db.file_source_root(file_id).source_root_id(db)) - .source_root(db) - .is_library - { - extractor.extract_with_semantics(file, &semantics, vfs, library_mode); - extractor.archiver.archive(file); - } + dependencies.add_file(manifest, file_id, file, &semantics, &source_files); } } else { for file in files { @@ -340,6 +327,19 @@ fn main() -> anyhow::Result<()> { } } } + + // For each workspace extract the dependency files for which that workspace + // is the preferred one. + for (manifest, files) in dependencies.by_manifests() { + let (db, vfs) = extractor + .load_manifest(manifest, &cargo_config, &load_cargo_config) + .with_context(|| format!("unable to reload workspace {}", manifest.manifest_path()))?; + let semantics = Semantics::new(&db); + for file in files { + extractor.extract_with_semantics(&file, &semantics, &vfs, library_mode); + } + } + let builtins_dir = env::var("CODEQL_EXTRACTOR_RUST_ROOT") .map(|path| Path::new(&path).join("tools").join("builtins"))?; let builtins = fs::read_dir(builtins_dir).context("failed to read builtins directory")?; diff --git a/rust/extractor/src/util.rs b/rust/extractor/src/util.rs new file mode 100644 index 000000000000..a12ca6de74a1 --- /dev/null +++ b/rust/extractor/src/util.rs @@ -0,0 +1,15 @@ +use std::cmp::Ordering; + +/// Compares options using a custom comparator, with `None` ordered before `Some`. +pub(crate) fn cmp_option_by( + left: &Option, + right: &Option, + compare: impl FnOnce(&T, &T) -> Ordering, +) -> Ordering { + match (left, right) { + (Some(left), Some(right)) => compare(left, right), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } +} From 2d23c27048a2b153c5aedd00ea6ce6f470d1894d Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 10:39:12 +0200 Subject: [PATCH 5/6] Rust: Test deterministic dependency configuration selection Add tests for extraction of dependency files with different features enabled in different manifest files. --- rust/extractor/src/configuration.rs | 17 ++-- rust/extractor/src/diagnostics.rs | 2 +- .../dependency-features/.cargo/config.toml | 7 ++ .../dependencies/alpha/.cargo-checksum.json | 1 + .../dependencies/alpha/Cargo.toml | 8 ++ .../dependencies/alpha/src/lib.rs | 21 +++++ .../dependencies/beta/.cargo-checksum.json | 1 + .../dependencies/beta/Cargo.toml | 8 ++ .../dependencies/beta/src/lib.rs | 21 +++++ .../dependency-features/functions.ql | 13 ++++ .../functions.rich.expected | 4 + .../functions.rich.reversed.expected | 4 + .../functions.tie.expected | 4 + .../functions.tie.reversed.expected | 4 + .../source/first/src/lib.rs | 4 + .../source/second/src/lib.rs | 4 + .../test_dependency_features.py | 78 +++++++++++++++++++ 17 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 rust/ql/integration-tests/dependency-features/.cargo/config.toml create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/functions.ql create mode 100644 rust/ql/integration-tests/dependency-features/functions.rich.expected create mode 100644 rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected create mode 100644 rust/ql/integration-tests/dependency-features/functions.tie.expected create mode 100644 rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected create mode 100644 rust/ql/integration-tests/dependency-features/source/first/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/source/second/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/test_dependency_features.py diff --git a/rust/extractor/src/configuration.rs b/rust/extractor/src/configuration.rs index b1bf13a8db3c..d07e92528a13 100644 --- a/rust/extractor/src/configuration.rs +++ b/rust/extractor/src/configuration.rs @@ -126,20 +126,19 @@ mod tests { } cfg }; - let root = crate::cwd().unwrap(); - let manifest = |path| ProjectManifest::from_manifest_file(root.join(path)).unwrap(); let candidates = [ - (manifest("z/Cargo.toml"), cfg(&["z"])), - (manifest("a/Cargo.toml"), cfg(&["a", "b"])), - (manifest("b/Cargo.toml"), cfg(&["a", "b"])), + ("z", cfg(&["z"])), + ("a", cfg(&["a", "b"])), + ("b", cfg(&["a", "b"])), ]; for order in [[0, 1, 2], [2, 1, 0]] { let mut plan = DependencyFiles::default(); - for (manifest, cfg) in order.map(|index| &candidates[index]) { - plan.add_candidate(Path::new("lib.rs"), Some(cfg.clone()), manifest); + for index in order { + let (workspace, cfg) = &candidates[index]; + plan.add_candidate(Path::new("lib.rs"), Some(cfg.clone()), Path::new(workspace)); } - let expected = HashMap::from([(&candidates[2].0, vec![PathBuf::from("lib.rs")])]); - assert_eq!(plan.by_manifests(), expected); + let expected = HashMap::from([(Path::new("b"), vec![PathBuf::from("lib.rs")])]); + assert_eq!(plan.by_workspaces(), expected); } } } diff --git a/rust/extractor/src/diagnostics.rs b/rust/extractor/src/diagnostics.rs index 541cef1309e8..ab88192c5d3e 100644 --- a/rust/extractor/src/diagnostics.rs +++ b/rust/extractor/src/diagnostics.rs @@ -292,7 +292,7 @@ mod tests { use super::*; #[test] - fn counts_unique_manifests() { + fn counts_unique_manifests_but_includes_reload_duration() { let steps: Vec<_> = ["a/Cargo.toml", "b/Cargo.toml", "a/Cargo.toml"] .into_iter() .map(|path| ExtractionStep { diff --git a/rust/ql/integration-tests/dependency-features/.cargo/config.toml b/rust/ql/integration-tests/dependency-features/.cargo/config.toml new file mode 100644 index 000000000000..6f5d1edd7231 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/.cargo/config.toml @@ -0,0 +1,7 @@ +# Add a source definition backed by the directories in "dependencies" +[source.test-dependencies] +directory = "dependencies" + +# Map crates.io to our local source definition +[source.crates-io] +replace-with = "test-dependencies" diff --git a/rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json b/rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json new file mode 100644 index 000000000000..09bf61c6fa41 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json @@ -0,0 +1 @@ +{"files": {}, "package": null} diff --git a/rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml b/rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml new file mode 100644 index 000000000000..151c6e5949f1 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "alpha" +version = "0.1.0" +edition = "2024" + +[features] +a = [] +b = [] diff --git a/rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs b/rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs new file mode 100644 index 000000000000..c2aafb3b633e --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs @@ -0,0 +1,21 @@ +#[cfg(feature = "a")] +pub fn a() {} + +#[cfg(feature = "b")] +pub fn b() {} + +#[cfg(feature = "b")] +#[macro_export] +macro_rules! invoke { + () => { + $crate::b() + }; +} + +#[cfg(not(feature = "b"))] +#[macro_export] +macro_rules! invoke { + () => { + $crate::a() + }; +} diff --git a/rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json b/rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json new file mode 100644 index 000000000000..09bf61c6fa41 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json @@ -0,0 +1 @@ +{"files": {}, "package": null} diff --git a/rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml b/rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml new file mode 100644 index 000000000000..b9c6e4f70445 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "beta" +version = "0.1.0" +edition = "2024" + +[features] +a = [] +b = [] diff --git a/rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs b/rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs new file mode 100644 index 000000000000..c2aafb3b633e --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs @@ -0,0 +1,21 @@ +#[cfg(feature = "a")] +pub fn a() {} + +#[cfg(feature = "b")] +pub fn b() {} + +#[cfg(feature = "b")] +#[macro_export] +macro_rules! invoke { + () => { + $crate::b() + }; +} + +#[cfg(not(feature = "b"))] +#[macro_export] +macro_rules! invoke { + () => { + $crate::a() + }; +} diff --git a/rust/ql/integration-tests/dependency-features/functions.ql b/rust/ql/integration-tests/dependency-features/functions.ql new file mode 100644 index 000000000000..a475c9258072 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.ql @@ -0,0 +1,13 @@ +import rust + +from Call c, string target +where + c.getFile().getRelativePath() in [ + "first/src/lib.rs", "second/src/lib.rs", "src/lib.rs", "build.rs" + ] and + ( + target = c.getStaticTarget().getCanonicalPath() + or + not exists(c.getStaticTarget()) and target = "" + ) +select c, target diff --git a/rust/ql/integration-tests/dependency-features/functions.rich.expected b/rust/ql/integration-tests/dependency-features/functions.rich.expected new file mode 100644 index 000000000000..796c4e1aae87 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.rich.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/first/src/lib.rs:3:5:3:17 | ...::a(...) | beta::a | +| source/second/src/lib.rs:2:5:2:18 | ...::a(...) | alpha::a | +| source/second/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | diff --git a/rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected b/rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected new file mode 100644 index 000000000000..f352f77ba179 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::a(...) | alpha::a | +| source/first/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | +| source/second/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/second/src/lib.rs:3:5:3:17 | ...::a(...) | beta::a | diff --git a/rust/ql/integration-tests/dependency-features/functions.tie.expected b/rust/ql/integration-tests/dependency-features/functions.tie.expected new file mode 100644 index 000000000000..31cbfba529fa --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.tie.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::a(...) | | +| source/first/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | +| source/second/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/second/src/lib.rs:3:5:3:17 | ...::a(...) | | diff --git a/rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected b/rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected new file mode 100644 index 000000000000..a4475eb87c83 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/first/src/lib.rs:3:5:3:17 | ...::a(...) | | +| source/second/src/lib.rs:2:5:2:18 | ...::a(...) | | +| source/second/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | diff --git a/rust/ql/integration-tests/dependency-features/source/first/src/lib.rs b/rust/ql/integration-tests/dependency-features/source/first/src/lib.rs new file mode 100644 index 000000000000..a4a49f20b4ce --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/source/first/src/lib.rs @@ -0,0 +1,4 @@ +pub fn emit() { + alpha::invoke!(); + beta::invoke!(); +} diff --git a/rust/ql/integration-tests/dependency-features/source/second/src/lib.rs b/rust/ql/integration-tests/dependency-features/source/second/src/lib.rs new file mode 100644 index 000000000000..a4a49f20b4ce --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/source/second/src/lib.rs @@ -0,0 +1,4 @@ +pub fn emit() { + alpha::invoke!(); + beta::invoke!(); +} diff --git a/rust/ql/integration-tests/dependency-features/test_dependency_features.py b/rust/ql/integration-tests/dependency-features/test_dependency_features.py new file mode 100644 index 000000000000..074109132d20 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/test_dependency_features.py @@ -0,0 +1,78 @@ +import json +from textwrap import dedent + +import commands +import pytest + + +@pytest.mark.parametrize( + ("configurations", "reverse"), + [ + pytest.param( + configurations, + reverse, + marks=pytest.mark.ql_test( + expected=f".{expected}{'.reversed' if reverse else ''}.expected" + ), + id=f"{name}{'-reversed' if reverse else ''}", + ) + for name, expected, configurations in [ + ("more-features", "rich", [(["a", "b"], ["a"]), (["a"], ["a", "b"])]), + ("equal-features", "tie", [(["a"], ["b"]), (["b"], ["a"])]), + ] + for reverse in [False, True] + ], +) +def test_dependency_features(codeql, rust, cwd, configurations, reverse): + """ + Two independent workspaces, `source/first` and `source/second`, share the + local dependencies `alpha` and `beta`, but enable different Cargo features + (their `Cargo.toml` are created in this test). + + Below is an overview of the files in the test setup. + ``` + dependency-features/ + |-- dependencies/ + | |-- alpha/ + | | |-- Cargo.toml + | | `-- src/lib.rs # feature-gated functions and invoke! macro + | `-- beta/ + | |-- Cargo.toml + | `-- src/lib.rs # feature-gated functions and invoke! macro + |-- source/ + | |-- first/ + | | |-- Cargo.toml # generated by this test for each configuration + | | `-- src/lib.rs # calls alpha::invoke! and beta::invoke! + | `-- second/ + | |-- Cargo.toml # generated by this test for each configuration + | `-- src/lib.rs # calls alpha::invoke! and beta::invoke! + ``` + + Each `src/lib.rs` file calls a macro from each dependency. The macros expand + to different calls depending on the enabled features. The `functions.ql` + query checks which calls and targets are extracted for the `src/lib.rs` + files. + """ + if reverse: + configurations = configurations[::-1] + for workspace, features in zip(["first", "second"], configurations): + manifest = cwd / "source" / workspace / "Cargo.toml" + manifest.write_text( + dedent(f"""\ + [package] + name = "{workspace}" + version = "0.1.0" + edition = "2024" + + [workspace] + + [dependencies] + """) + + "\n".join( + f'{name} = {{ version = "=0.1.0", features = {json.dumps(enabled)} }}' + for name, enabled in zip(["alpha", "beta"], features) + ) + + "\n" + ) + commands.run(f'cargo check --quiet --manifest-path "{manifest}"') + codeql.database.create(source_root="source") From a3e8174ba69fb11e93a3ff8ff9135b144603fdae Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 11:29:31 +0200 Subject: [PATCH 6/6] Rust: Update extraction-step expectations for dependency reloads Regenerate existing integration-test expectations to account for manifest reloads in the deferred dependency extraction pass. --- rust/ql/integration-tests/hello-project/steps.cargo.expected | 1 + .../integration-tests/hello-project/steps.rust-project.expected | 1 + rust/ql/integration-tests/hello-workspace/steps.cargo.expected | 1 + .../hello-workspace/steps.rust-project.expected | 1 + rust/ql/integration-tests/workspace-with-glob/steps.expected | 1 + 5 files changed, 5 insertions(+) diff --git a/rust/ql/integration-tests/hello-project/steps.cargo.expected b/rust/ql/integration-tests/hello-project/steps.cargo.expected index 4deec0653daf..9517685e46a1 100644 --- a/rust/ql/integration-tests/hello-project/steps.cargo.expected +++ b/rust/ql/integration-tests/hello-project/steps.cargo.expected @@ -1,4 +1,5 @@ | Cargo.toml:0:0:0:0 | LoadManifest(Cargo.toml) | +| Cargo.toml:0:0:0:0 | LoadManifest(Cargo.toml) | | file://:0:0:0:0 | CrateGraph | | file://:0:0:0:0 | FindManifests | | src/directory_module/mod.rs:0:0:0:0 | Extract(src/directory_module/mod.rs) | diff --git a/rust/ql/integration-tests/hello-project/steps.rust-project.expected b/rust/ql/integration-tests/hello-project/steps.rust-project.expected index fa790e6cd7fd..a49d578c082e 100644 --- a/rust/ql/integration-tests/hello-project/steps.rust-project.expected +++ b/rust/ql/integration-tests/hello-project/steps.rust-project.expected @@ -1,6 +1,7 @@ | file://:0:0:0:0 | CrateGraph | | file://:0:0:0:0 | FindManifests | | rust-project.json:0:0:0:0 | LoadManifest(rust-project.json) | +| rust-project.json:0:0:0:0 | LoadManifest(rust-project.json) | | src/directory_module/mod.rs:0:0:0:0 | Extract(src/directory_module/mod.rs) | | src/directory_module/mod.rs:0:0:0:0 | LoadSource(src/directory_module/mod.rs) | | src/directory_module/mod.rs:0:0:0:0 | Parse(src/directory_module/mod.rs) | diff --git a/rust/ql/integration-tests/hello-workspace/steps.cargo.expected b/rust/ql/integration-tests/hello-workspace/steps.cargo.expected index 32a3b1110247..873b5354a903 100644 --- a/rust/ql/integration-tests/hello-workspace/steps.cargo.expected +++ b/rust/ql/integration-tests/hello-workspace/steps.cargo.expected @@ -1,4 +1,5 @@ | Cargo.toml:0:0:0:0 | LoadManifest(Cargo.toml) | +| Cargo.toml:0:0:0:0 | LoadManifest(Cargo.toml) | | exe/src/a_module.rs:0:0:0:0 | Extract(exe/src/a_module.rs) | | exe/src/a_module.rs:0:0:0:0 | LoadSource(exe/src/a_module.rs) | | exe/src/a_module.rs:0:0:0:0 | Parse(exe/src/a_module.rs) | diff --git a/rust/ql/integration-tests/hello-workspace/steps.rust-project.expected b/rust/ql/integration-tests/hello-workspace/steps.rust-project.expected index e9a65e0c7be5..95b2de56bdb9 100644 --- a/rust/ql/integration-tests/hello-workspace/steps.rust-project.expected +++ b/rust/ql/integration-tests/hello-workspace/steps.rust-project.expected @@ -13,3 +13,4 @@ | lib/src/lib.rs:0:0:0:0 | LoadSource(lib/src/lib.rs) | | lib/src/lib.rs:0:0:0:0 | Parse(lib/src/lib.rs) | | rust-project.json:0:0:0:0 | LoadManifest(rust-project.json) | +| rust-project.json:0:0:0:0 | LoadManifest(rust-project.json) | diff --git a/rust/ql/integration-tests/workspace-with-glob/steps.expected b/rust/ql/integration-tests/workspace-with-glob/steps.expected index 4b0e6ed828b6..3ea9ee61bed3 100644 --- a/rust/ql/integration-tests/workspace-with-glob/steps.expected +++ b/rust/ql/integration-tests/workspace-with-glob/steps.expected @@ -6,6 +6,7 @@ | lib/src/lib.rs:0:0:0:0 | Extract(lib/src/lib.rs) | | lib/src/lib.rs:0:0:0:0 | Parse(lib/src/lib.rs) | | other/Cargo.toml:0:0:0:0 | LoadManifest(other/Cargo.toml) | +| other/Cargo.toml:0:0:0:0 | LoadManifest(other/Cargo.toml) | | other/src/lib.rs:0:0:0:0 | Extract(other/src/lib.rs) | | other/src/lib.rs:0:0:0:0 | LoadSource(other/src/lib.rs) | | other/src/lib.rs:0:0:0:0 | Parse(other/src/lib.rs) |