Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions rust/extractor/src/configuration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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_vfs::{FileId, VfsPath};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap, HashSet, btree_map::Entry};
use std::path::{Path, PathBuf};

/// 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<CfgOptions>,
workspace: &'a Path,
}

impl Ord for DependencySource<'_> {
fn cmp(&self, other: &Self) -> Ordering {
cmp_option_by(&self.cfg_options, &other.cfg_options, compare_cfg_options)
.then_with(|| self.workspace.cmp(other.workspace))
}
}

impl PartialOrd for DependencySource<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

/// Map dependency file paths to their highest-priority workspace context.
#[derive(Default)]
pub struct DependencyFiles<'a> {
files: BTreeMap<PathBuf, DependencySource<'a>>,
}

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,
workspace: &'a Path,
file_id: FileId,
file: &VfsPath,
semantics: &Semantics<'_, RootDatabase>,
source_files: &HashSet<PathBuf>,
) {
let db = semantics.db;
if let Some(file) = file.as_path().map(<_ as AsRef<Path>>::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, workspace);
}
}

fn add_candidate(&mut self, file: &Path, cfg_options: Option<CfgOptions>, workspace: &'a Path) {
let candidate = DependencySource {
cfg_options,
workspace,
};
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 workspace for the dependency extraction pass.
pub fn by_workspaces(self) -> HashMap<&'a Path, Vec<PathBuf>> {
self.files
.into_iter()
.map(|(file, source)| (source.workspace, 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 candidates = [
("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 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([(Path::new("b"), vec![PathBuf::from("lib.rs")])]);
assert_eq!(plan.by_workspaces(), expected);
}
}
}
33 changes: 29 additions & 4 deletions rust/extractor/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -220,13 +220,15 @@ struct ExtractionSummary {
type ExtractionDiagnostics = Diagnostics<ExtractionSummary>;

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;
Expand All @@ -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 },
}
Expand Down Expand Up @@ -284,3 +286,26 @@ 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_but_includes_reload_duration() {
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);
assert_eq!(
summary.durations.durations[&ExtractionStepKind::LoadManifest].0,
3
);
}
}
Loading
Loading