deno/cli/tools/registry/publish_order.rs
David Sherret 66424032a2
feat(unstable/lint): no-slow-types for JSR packages (#22430)
1. Renames zap/fast-check to instead be a `no-slow-types` lint rule.
1. This lint rule is automatically run when doing `deno lint` for
packages (deno.json files with a name, version, and exports field)
1. This lint rules still occurs on publish. It can be skipped by running
with `--no-slow-types`
2024-02-19 15:28:41 +00:00

292 lines
8.7 KiB
Rust

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use deno_ast::ModuleSpecifier;
use deno_config::WorkspaceMemberConfig;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_graph::ModuleGraph;
pub struct PublishOrderGraph {
packages: HashMap<String, HashSet<String>>,
in_degree: HashMap<String, usize>,
reverse_map: HashMap<String, Vec<String>>,
}
impl PublishOrderGraph {
pub fn next(&mut self) -> Vec<String> {
let mut package_names_with_depth = self
.in_degree
.iter()
.filter_map(|(name, &degree)| if degree == 0 { Some(name) } else { None })
.map(|item| (item.clone(), self.compute_depth(item, HashSet::new())))
.collect::<Vec<_>>();
// sort by depth to in order to prioritize those packages
package_names_with_depth.sort_by(|a, b| match b.1.cmp(&a.1) {
std::cmp::Ordering::Equal => a.0.cmp(&b.0),
other => other,
});
let sorted_package_names = package_names_with_depth
.into_iter()
.map(|(name, _)| name)
.collect::<Vec<_>>();
for name in &sorted_package_names {
self.in_degree.remove(name);
}
sorted_package_names
}
pub fn finish_package(&mut self, name: &str) {
if let Some(package_names) = self.reverse_map.remove(name) {
for name in package_names {
*self.in_degree.get_mut(&name).unwrap() -= 1;
}
}
}
/// There could be pending packages if there's a circular dependency.
pub fn ensure_no_pending(&self) -> Result<(), AnyError> {
// this is inefficient, but that's ok because it's simple and will
// only ever happen when there's an error
fn identify_cycle<'a>(
current_name: &'a String,
mut visited: HashSet<&'a String>,
packages: &HashMap<String, HashSet<String>>,
) -> Option<Vec<String>> {
if visited.insert(current_name) {
let deps = packages.get(current_name).unwrap();
for dep in deps {
if let Some(mut cycle) =
identify_cycle(dep, visited.clone(), packages)
{
cycle.push(current_name.to_string());
return Some(cycle);
}
}
None
} else {
Some(vec![current_name.to_string()])
}
}
if self.in_degree.is_empty() {
Ok(())
} else {
let mut pkg_names = self.in_degree.keys().collect::<Vec<_>>();
pkg_names.sort(); // determinism
let mut cycle =
identify_cycle(pkg_names[0], HashSet::new(), &self.packages).unwrap();
cycle.reverse();
bail!(
"Circular package dependency detected: {}",
cycle.join(" -> ")
);
}
}
fn compute_depth(
&self,
package_name: &String,
mut visited: HashSet<String>,
) -> usize {
if visited.contains(package_name) {
return 0; // cycle
}
visited.insert(package_name.clone());
let Some(parents) = self.reverse_map.get(package_name) else {
return 0;
};
let max_depth = parents
.iter()
.map(|child| self.compute_depth(child, visited.clone()))
.max()
.unwrap_or(0);
1 + max_depth
}
}
pub fn build_publish_order_graph(
graph: &ModuleGraph,
roots: &[WorkspaceMemberConfig],
) -> Result<PublishOrderGraph, AnyError> {
let packages = build_pkg_deps(graph, roots)?;
Ok(build_publish_order_graph_from_pkgs_deps(packages))
}
fn build_pkg_deps(
graph: &deno_graph::ModuleGraph,
roots: &[WorkspaceMemberConfig],
) -> Result<HashMap<String, HashSet<String>>, AnyError> {
let mut members = HashMap::with_capacity(roots.len());
let mut seen_modules = HashSet::with_capacity(graph.modules().count());
let roots = roots
.iter()
.map(|r| (ModuleSpecifier::from_file_path(&r.dir_path).unwrap(), r))
.collect::<Vec<_>>();
for (root_dir_url, root) in &roots {
let mut deps = HashSet::new();
let mut pending = VecDeque::new();
pending.extend(root.config_file.resolve_export_value_urls()?);
while let Some(specifier) = pending.pop_front() {
let Some(module) = graph.get(&specifier).and_then(|m| m.js()) else {
continue;
};
let mut dep_specifiers =
Vec::with_capacity(module.dependencies.len() + 1);
if let Some(types_dep) = &module.maybe_types_dependency {
if let Some(specifier) = types_dep.dependency.maybe_specifier() {
dep_specifiers.push(specifier);
}
}
for (_, dep) in &module.dependencies {
if let Some(specifier) = dep.maybe_code.maybe_specifier() {
dep_specifiers.push(specifier);
}
if let Some(specifier) = dep.maybe_type.maybe_specifier() {
dep_specifiers.push(specifier);
}
}
for specifier in dep_specifiers {
let specifier = graph.resolve(specifier);
if specifier.scheme() != "file" {
continue;
}
if specifier.as_str().starts_with(root_dir_url.as_str()) {
if seen_modules.insert(specifier.clone()) {
pending.push_back(specifier.clone());
}
} else {
let found_root = roots.iter().find(|(dir_url, _)| {
specifier.as_str().starts_with(dir_url.as_str())
});
if let Some(root) = found_root {
deps.insert(root.1.package_name.clone());
}
}
}
}
members.insert(root.package_name.clone(), deps);
}
Ok(members)
}
fn build_publish_order_graph_from_pkgs_deps(
packages: HashMap<String, HashSet<String>>,
) -> PublishOrderGraph {
let mut in_degree = HashMap::new();
let mut reverse_map: HashMap<String, Vec<String>> = HashMap::new();
// build the graph, in-degree map, and set of all nodes
for (pkg_name, deps) in &packages {
in_degree.insert(pkg_name.clone(), deps.len());
for dep in deps {
reverse_map
.entry(dep.clone())
.or_default()
.push(pkg_name.clone());
}
}
PublishOrderGraph {
packages: packages.clone(),
in_degree,
reverse_map,
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_graph_no_deps() {
let mut graph = build_publish_order_graph_from_pkgs_deps(HashMap::from([
("a".to_string(), HashSet::new()),
("b".to_string(), HashSet::new()),
("c".to_string(), HashSet::new()),
]));
assert_eq!(
graph.next(),
vec!["a".to_string(), "b".to_string(), "c".to_string()],
);
graph.finish_package("a");
assert!(graph.next().is_empty());
graph.finish_package("b");
assert!(graph.next().is_empty());
graph.finish_package("c");
assert!(graph.next().is_empty());
graph.ensure_no_pending().unwrap();
}
#[test]
fn test_graph_single_dep() {
let mut graph = build_publish_order_graph_from_pkgs_deps(HashMap::from([
("a".to_string(), HashSet::from(["b".to_string()])),
("b".to_string(), HashSet::from(["c".to_string()])),
("c".to_string(), HashSet::new()),
]));
assert_eq!(graph.next(), vec!["c".to_string()]);
graph.finish_package("c");
assert_eq!(graph.next(), vec!["b".to_string()]);
graph.finish_package("b");
assert_eq!(graph.next(), vec!["a".to_string()]);
graph.finish_package("a");
assert!(graph.next().is_empty());
graph.ensure_no_pending().unwrap();
}
#[test]
fn test_graph_multiple_dep() {
let mut graph = build_publish_order_graph_from_pkgs_deps(HashMap::from([
(
"a".to_string(),
HashSet::from(["b".to_string(), "c".to_string()]),
),
("b".to_string(), HashSet::from(["c".to_string()])),
("c".to_string(), HashSet::new()),
("d".to_string(), HashSet::new()),
("e".to_string(), HashSet::from(["f".to_string()])),
("f".to_string(), HashSet::new()),
]));
assert_eq!(
graph.next(),
vec!["c".to_string(), "f".to_string(), "d".to_string()]
);
graph.finish_package("f");
assert_eq!(graph.next(), vec!["e".to_string()]);
graph.finish_package("e");
assert!(graph.next().is_empty());
graph.finish_package("d");
assert!(graph.next().is_empty());
graph.finish_package("c");
assert_eq!(graph.next(), vec!["b".to_string()]);
graph.finish_package("b");
assert_eq!(graph.next(), vec!["a".to_string()]);
graph.finish_package("a");
assert!(graph.next().is_empty());
graph.ensure_no_pending().unwrap();
}
#[test]
fn test_graph_circular_dep() {
let mut graph = build_publish_order_graph_from_pkgs_deps(HashMap::from([
("a".to_string(), HashSet::from(["b".to_string()])),
("b".to_string(), HashSet::from(["c".to_string()])),
("c".to_string(), HashSet::from(["a".to_string()])),
]));
assert!(graph.next().is_empty());
assert_eq!(
graph.ensure_no_pending().unwrap_err().to_string(),
"Circular package dependency detected: a -> b -> c -> a"
);
}
}