Skip to main content

git_policy_cli/
dependent_version_bundled.rs

1//! What: Decide which development dependencies each dependent bundles, reading source
2//!       only where an edge could carry a raised version.
3//! Why: A development dependency ships inside a dependent only when its non-test source
4//!      imports it. Scanning every package's source on every commit would read the whole
5//!      workspace, so, as in the incumbent (`dependent-bump-workflow.ts:176-278`), only
6//!      dependents that could reach a raised package if every development dependency were
7//!      bundled are scanned, and only for edges that lead toward a raised package.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! // await bundledDevelopmentEdges({ reader, states, bumpedNames });
12//! ```
13
14/// What: `use` brings names from sibling files and the standard library into this file.
15/// Why:  The superset walk is the plan's own walk; source paths and imports have their
16///       own modules.
17///
18/// In TS you'd write (pseudocode):
19/// ```ts
20/// import { transitiveDependentNames } from './dependent_version_graph.ts';
21/// ```
22use super::dependent_version_content::{ContentUnavailable, TrackedPath, WorkspaceContent};
23/// The superset walk over every edge.
24use super::dependent_version_graph::{WorkspaceNode, transitive_dependent_names};
25/// The specifier scan.
26use super::dependent_version_imports::imports_package;
27/// Which tracked paths are a package's non-test source.
28use super::dependent_version_paths::is_non_test_source_path;
29/// One manifest's facts and directory.
30use super::dependent_version_states::ManifestState;
31/// A hash-based set of relevant names.
32use std::collections::HashSet;
33
34/// What: The development dependencies of one dependent that could carry a raised version.
35/// Why:  An edge to a package outside the superset and the raised set leads nowhere; a self
36///       edge is never an edge.
37///
38/// In TS you'd write (pseudocode):
39/// ```ts
40/// state.current.devDependencyNames.filter(name => relevant.has(name) && name !== state.current.name);
41/// ```
42fn candidate_edges(state: &ManifestState, relevant: &HashSet<String>) -> Vec<String> {
43    return state
44        .facts
45        .dev_dependency_names
46        .iter()
47        .filter(|name: &&String| return relevant.contains(*name) && **name != state.facts.name)
48        .cloned()
49        .collect();
50}
51
52/// What: The texts of a package's non-test source files. `String::from_utf8_lossy`
53///       replaces bytes that are not UTF-8 with U+FFFD, as the incumbent's lenient decoder
54///       does.
55/// Why:  A source file only needs scanning for specifiers, so undecodable bytes are not a
56///       failure.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// await Promise.all((await reader.sourceFiles(directory)).filter(isSource).map(file => file.text()));
61/// ```
62fn source_texts(
63    content: &mut dyn WorkspaceContent,
64    tracked: &[TrackedPath],
65    directory: &[u8],
66) -> Result<Vec<String>, ContentUnavailable> {
67    let mut texts: Vec<String> = Vec::new();
68    for entry in tracked {
69        if is_non_test_source_path(directory, &entry.path) {
70            let bytes: Vec<u8> = content.candidate_bytes(&entry.path)?;
71            texts.push(String::from_utf8_lossy(&bytes).into_owned());
72        }
73    }
74    return Ok(texts);
75}
76
77/// What: For each state, in order, the development dependencies its source imports among
78///       those that could carry a raised version. `Vec<Vec<String>>` is one list per state.
79/// Why:  These become edges of the plan's walk alongside the runtime edges.
80///
81/// In TS you'd write (pseudocode):
82/// ```ts
83/// async function bundledDevelopmentEdges({ reader, states, bumpedNames }): Map<string, string[]>;
84/// ```
85pub fn bundled_development_edges(
86    content: &mut dyn WorkspaceContent,
87    tracked: &[TrackedPath],
88    states: &[ManifestState],
89    bumped_names: &[String],
90) -> Result<Vec<Vec<String>>, ContentUnavailable> {
91    let every_edge: Vec<WorkspaceNode> = states
92        .iter()
93        .map(|state: &ManifestState| {
94            return WorkspaceNode {
95                name: state.facts.name.clone(),
96                directory: state.directory.clone(),
97                version: None,
98                edge_names: [
99                    state.facts.runtime_dependency_names.as_slice(),
100                    state.facts.dev_dependency_names.as_slice(),
101                ]
102                .concat(),
103            };
104        })
105        .collect();
106    let mut relevant: HashSet<String> = transitive_dependent_names(&every_edge, bumped_names);
107    relevant.extend(bumped_names.iter().cloned());
108    let mut bundled: Vec<Vec<String>> = Vec::new();
109    for state in states {
110        // The incumbent also skips a dependent outside the superset. That test is implied:
111        // an edge to a relevant workspace package puts the dependent in the superset.
112        let candidates: Vec<String> = candidate_edges(state, &relevant);
113        if candidates.is_empty() {
114            bundled.push(Vec::new());
115            continue;
116        }
117        let texts: Vec<String> = source_texts(content, tracked, &state.directory)?;
118        bundled.push(
119            candidates
120                .into_iter()
121                .filter(|name: &String| {
122                    return texts
123                        .iter()
124                        .any(|text: &String| return imports_package(text, name));
125                })
126                .collect(),
127        );
128    }
129    return Ok(bundled);
130}
131
132/// Which dependents are scanned, which edges are confirmed, and which files are read.
133#[cfg(test)]
134#[path = "dependent_version_bundled_tests.rs"]
135mod tests;