From 4bac09f58fdc3d847b3231dcf64b2d02f4eac403 Mon Sep 17 00:00:00 2001
From: Noah Lev <camelidcamel@gmail.com>
Date: Mon, 13 Dec 2021 12:42:01 -0800
Subject: [PATCH] rustdoc: Add `UrlPartsBuilder`

This is a type for efficiently and easily constructing the part of a URL
after the domain: `nightly/core/str/struct.Bytes.html`.

It allows simplifying some code and avoiding some allocations in the
`href_*` functions.

It will also allow making `Cache.paths` et al. use `Symbol` without
having to allocate `String`s in the `href_*` functions. `String`s would
be necessary otherwise because `Symbol::as_str()` returns `SymbolStr`,
whose `Deref<Target = str>` impl requires the `str` to not outlive it.
This is the primary motivation for the addition of `UrlPartsBuilder`.
---
 src/librustdoc/html/format.rs                 |  19 +--
 src/librustdoc/html/mod.rs                    |   1 +
 src/librustdoc/html/tests.rs                  |  16 +--
 src/librustdoc/html/url_parts_builder.rs      | 119 ++++++++++++++++++
 .../html/url_parts_builder/tests.rs           |  54 ++++++++
 5 files changed, 192 insertions(+), 17 deletions(-)
 create mode 100644 src/librustdoc/html/url_parts_builder.rs
 create mode 100644 src/librustdoc/html/url_parts_builder/tests.rs

diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index a3cbb5756fefc..f6788e94431dc 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -27,6 +27,8 @@ use crate::html::escape::Escape;
 use crate::html::render::cache::ExternalLocation;
 use crate::html::render::Context;
 
+use super::url_parts_builder::UrlPartsBuilder;
+
 crate trait Print {
     fn print(self, buffer: &mut Buffer);
 }
@@ -544,9 +546,9 @@ crate fn href_with_root_path(
                         ExternalLocation::Remote(ref s) => {
                             is_remote = true;
                             let s = s.trim_end_matches('/');
-                            let mut s = vec![s];
-                            s.extend(module_fqp[..].iter().map(String::as_str));
-                            s
+                            let mut builder = UrlPartsBuilder::singleton(s);
+                            builder.extend(module_fqp.iter().map(String::as_str));
+                            builder
                         }
                         ExternalLocation::Local => href_relative_parts(module_fqp, relative_to),
                         ExternalLocation::Unknown => return Err(HrefError::DocumentationNotBuilt),
@@ -560,22 +562,21 @@ crate fn href_with_root_path(
     if !is_remote {
         if let Some(root_path) = root_path {
             let root = root_path.trim_end_matches('/');
-            url_parts.insert(0, root);
+            url_parts.push_front(root);
         }
     }
     debug!(?url_parts);
     let last = &fqp.last().unwrap()[..];
-    let filename;
     match shortty {
         ItemType::Module => {
             url_parts.push("index.html");
         }
         _ => {
-            filename = format!("{}.{}.html", shortty.as_str(), last);
+            let filename = format!("{}.{}.html", shortty.as_str(), last);
             url_parts.push(&filename);
         }
     }
-    Ok((url_parts.join("/"), shortty, fqp.to_vec()))
+    Ok((url_parts.finish(), shortty, fqp.to_vec()))
 }
 
 crate fn href(did: DefId, cx: &Context<'_>) -> Result<(String, ItemType, Vec<String>), HrefError> {
@@ -585,7 +586,7 @@ crate fn href(did: DefId, cx: &Context<'_>) -> Result<(String, ItemType, Vec<Str
 /// Both paths should only be modules.
 /// This is because modules get their own directories; that is, `std::vec` and `std::vec::Vec` will
 /// both need `../iter/trait.Iterator.html` to get at the iterator trait.
-crate fn href_relative_parts<'a>(fqp: &'a [String], relative_to_fqp: &'a [String]) -> Vec<&'a str> {
+crate fn href_relative_parts(fqp: &[String], relative_to_fqp: &[String]) -> UrlPartsBuilder {
     for (i, (f, r)) in fqp.iter().zip(relative_to_fqp.iter()).enumerate() {
         // e.g. linking to std::iter from std::vec (`dissimilar_part_count` will be 1)
         if f != r {
@@ -603,7 +604,7 @@ crate fn href_relative_parts<'a>(fqp: &'a [String], relative_to_fqp: &'a [String
         iter::repeat("..").take(dissimilar_part_count).collect()
     // linking to the same module
     } else {
-        Vec::new()
+        UrlPartsBuilder::new()
     }
 }
 
diff --git a/src/librustdoc/html/mod.rs b/src/librustdoc/html/mod.rs
index 109b0a356db5f..e1bbc784fd1cd 100644
--- a/src/librustdoc/html/mod.rs
+++ b/src/librustdoc/html/mod.rs
@@ -9,6 +9,7 @@ crate mod render;
 crate mod sources;
 crate mod static_files;
 crate mod toc;
+mod url_parts_builder;
 
 #[cfg(test)]
 mod tests;
diff --git a/src/librustdoc/html/tests.rs b/src/librustdoc/html/tests.rs
index 5d537dabd0c07..dee9f5e5038a8 100644
--- a/src/librustdoc/html/tests.rs
+++ b/src/librustdoc/html/tests.rs
@@ -1,44 +1,44 @@
 use crate::html::format::href_relative_parts;
 
-fn assert_relative_path(expected: &[&str], relative_to_fqp: &[&str], fqp: &[&str]) {
+fn assert_relative_path(expected: &str, relative_to_fqp: &[&str], fqp: &[&str]) {
     let relative_to_fqp: Vec<String> = relative_to_fqp.iter().copied().map(String::from).collect();
     let fqp: Vec<String> = fqp.iter().copied().map(String::from).collect();
-    assert_eq!(expected, href_relative_parts(&fqp, &relative_to_fqp));
+    assert_eq!(expected, href_relative_parts(&fqp, &relative_to_fqp).finish());
 }
 
 #[test]
 fn href_relative_parts_basic() {
     let relative_to_fqp = &["std", "vec"];
     let fqp = &["std", "iter"];
-    assert_relative_path(&["..", "iter"], relative_to_fqp, fqp);
+    assert_relative_path("../iter", relative_to_fqp, fqp);
 }
 #[test]
 fn href_relative_parts_parent_module() {
     let relative_to_fqp = &["std", "vec"];
     let fqp = &["std"];
-    assert_relative_path(&[".."], relative_to_fqp, fqp);
+    assert_relative_path("..", relative_to_fqp, fqp);
 }
 #[test]
 fn href_relative_parts_different_crate() {
     let relative_to_fqp = &["std", "vec"];
     let fqp = &["core", "iter"];
-    assert_relative_path(&["..", "..", "core", "iter"], relative_to_fqp, fqp);
+    assert_relative_path("../../core/iter", relative_to_fqp, fqp);
 }
 #[test]
 fn href_relative_parts_same_module() {
     let relative_to_fqp = &["std", "vec"];
     let fqp = &["std", "vec"];
-    assert_relative_path(&[], relative_to_fqp, fqp);
+    assert_relative_path("", relative_to_fqp, fqp);
 }
 #[test]
 fn href_relative_parts_child_module() {
     let relative_to_fqp = &["std"];
     let fqp = &["std", "vec"];
-    assert_relative_path(&["vec"], relative_to_fqp, fqp);
+    assert_relative_path("vec", relative_to_fqp, fqp);
 }
 #[test]
 fn href_relative_parts_root() {
     let relative_to_fqp = &[];
     let fqp = &["std"];
-    assert_relative_path(&["std"], relative_to_fqp, fqp);
+    assert_relative_path("std", relative_to_fqp, fqp);
 }
diff --git a/src/librustdoc/html/url_parts_builder.rs b/src/librustdoc/html/url_parts_builder.rs
new file mode 100644
index 0000000000000..918d5e6bd1b3a
--- /dev/null
+++ b/src/librustdoc/html/url_parts_builder.rs
@@ -0,0 +1,119 @@
+/// A builder that allows efficiently and easily constructing the part of a URL
+/// after the domain: `nightly/core/str/struct.Bytes.html`.
+///
+/// This type is a wrapper around the final `String` buffer,
+/// but its API is like that of a `Vec` of URL components.
+#[derive(Debug)]
+crate struct UrlPartsBuilder {
+    buf: String,
+}
+
+impl UrlPartsBuilder {
+    /// Create an empty buffer.
+    crate fn new() -> Self {
+        Self { buf: String::new() }
+    }
+
+    /// Create an empty buffer with capacity for the specified number of bytes.
+    fn with_capacity_bytes(count: usize) -> Self {
+        Self { buf: String::with_capacity(count) }
+    }
+
+    /// Create a buffer with one URL component.
+    ///
+    /// # Examples
+    ///
+    /// Basic usage:
+    ///
+    /// ```ignore (private-type)
+    /// let builder = UrlPartsBuilder::singleton("core");
+    /// assert_eq!(builder.finish(), "core");
+    /// ```
+    ///
+    /// Adding more components afterward.
+    ///
+    /// ```ignore (private-type)
+    /// let mut builder = UrlPartsBuilder::singleton("core");
+    /// builder.push("str");
+    /// builder.push_front("nightly");
+    /// assert_eq!(builder.finish(), "nightly/core/str");
+    /// ```
+    crate fn singleton(part: &str) -> Self {
+        Self { buf: part.to_owned() }
+    }
+
+    /// Push a component onto the buffer.
+    ///
+    /// # Examples
+    ///
+    /// Basic usage:
+    ///
+    /// ```ignore (private-type)
+    /// let mut builder = UrlPartsBuilder::new();
+    /// builder.push("core");
+    /// builder.push("str");
+    /// builder.push("struct.Bytes.html");
+    /// assert_eq!(builder.finish(), "core/str/struct.Bytes.html");
+    /// ```
+    crate fn push(&mut self, part: &str) {
+        if !self.buf.is_empty() {
+            self.buf.push('/');
+        }
+        self.buf.push_str(part);
+    }
+
+    /// Push a component onto the front of the buffer.
+    ///
+    /// # Examples
+    ///
+    /// Basic usage:
+    ///
+    /// ```ignore (private-type)
+    /// let mut builder = UrlPartsBuilder::new();
+    /// builder.push("core");
+    /// builder.push("str");
+    /// builder.push_front("nightly");
+    /// builder.push("struct.Bytes.html");
+    /// assert_eq!(builder.finish(), "nightly/core/str/struct.Bytes.html");
+    /// ```
+    crate fn push_front(&mut self, part: &str) {
+        let is_empty = self.buf.is_empty();
+        self.buf.reserve(part.len() + if !is_empty { 1 } else { 0 });
+        self.buf.insert_str(0, part);
+        if !is_empty {
+            self.buf.insert(part.len(), '/');
+        }
+    }
+
+    /// Get the final `String` buffer.
+    crate fn finish(self) -> String {
+        self.buf
+    }
+}
+
+/// This is just a guess at the average length of a URL part,
+/// used for [`String::with_capacity`] calls in the [`FromIterator`]
+/// and [`Extend`] impls.
+///
+/// This is intentionally on the lower end to avoid overallocating.
+const AVG_PART_LENGTH: usize = 5;
+
+impl<'a> FromIterator<&'a str> for UrlPartsBuilder {
+    fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
+        let iter = iter.into_iter();
+        let mut builder = Self::with_capacity_bytes(AVG_PART_LENGTH * iter.size_hint().0);
+        iter.for_each(|part| builder.push(part));
+        builder
+    }
+}
+
+impl<'a> Extend<&'a str> for UrlPartsBuilder {
+    fn extend<T: IntoIterator<Item = &'a str>>(&mut self, iter: T) {
+        let iter = iter.into_iter();
+        self.buf.reserve(AVG_PART_LENGTH * iter.size_hint().0);
+        iter.for_each(|part| self.push(part));
+    }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/src/librustdoc/html/url_parts_builder/tests.rs b/src/librustdoc/html/url_parts_builder/tests.rs
new file mode 100644
index 0000000000000..43338c95010a0
--- /dev/null
+++ b/src/librustdoc/html/url_parts_builder/tests.rs
@@ -0,0 +1,54 @@
+use super::*;
+
+fn t(builder: UrlPartsBuilder, expect: &str) {
+    assert_eq!(builder.finish(), expect);
+}
+
+#[test]
+fn empty() {
+    t(UrlPartsBuilder::new(), "");
+}
+
+#[test]
+fn singleton() {
+    t(UrlPartsBuilder::singleton("index.html"), "index.html");
+}
+
+#[test]
+fn push_several() {
+    let mut builder = UrlPartsBuilder::new();
+    builder.push("core");
+    builder.push("str");
+    builder.push("struct.Bytes.html");
+    t(builder, "core/str/struct.Bytes.html");
+}
+
+#[test]
+fn push_front_empty() {
+    let mut builder = UrlPartsBuilder::new();
+    builder.push_front("page.html");
+    t(builder, "page.html");
+}
+
+#[test]
+fn push_front_non_empty() {
+    let mut builder = UrlPartsBuilder::new();
+    builder.push("core");
+    builder.push("str");
+    builder.push("struct.Bytes.html");
+    builder.push_front("nightly");
+    t(builder, "nightly/core/str/struct.Bytes.html");
+}
+
+#[test]
+fn collect() {
+    t(["core", "str"].into_iter().collect(), "core/str");
+    t(["core", "str", "struct.Bytes.html"].into_iter().collect(), "core/str/struct.Bytes.html");
+}
+
+#[test]
+fn extend() {
+    let mut builder = UrlPartsBuilder::singleton("core");
+    builder.extend(["str", "struct.Bytes.html"]);
+    t(builder, "core/str/struct.Bytes.html");
+}