diff --git a/ruff_dev/src/round_trip.rs b/ruff_dev/src/round_trip.rs
index 3faa5d6c279d0d..1df3fdfb3ef529 100644
--- a/ruff_dev/src/round_trip.rs
+++ b/ruff_dev/src/round_trip.rs
@@ -5,8 +5,7 @@ use std::path::PathBuf;
 
 use anyhow::Result;
 use clap::Args;
-use ruff::source_code::{Generator, Locator, Stylist};
-use rustpython_parser::parser;
+use ruff::source_code::round_trip;
 
 #[derive(Args)]
 pub struct Cli {
@@ -17,11 +16,6 @@ pub struct Cli {
 
 pub fn main(cli: &Cli) -> Result<()> {
     let contents = fs::read_to_string(&cli.file)?;
-    let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
-    let locator = Locator::new(&contents);
-    let stylist = Stylist::from_contents(&contents, &locator);
-    let mut generator: Generator = (&stylist).into();
-    generator.unparse_suite(&python_ast);
-    println!("{}", generator.generate());
+    println!("{}", round_trip(&contents, &cli.file.to_string_lossy())?);
     Ok(())
 }
diff --git a/src/source_code/locator.rs b/src/source_code/locator.rs
index 6e94d4fddbecbe..949b16976c6cc1 100644
--- a/src/source_code/locator.rs
+++ b/src/source_code/locator.rs
@@ -25,12 +25,14 @@ impl<'a> Locator<'a> {
         self.rope.get_or_init(|| Rope::from_str(self.contents))
     }
 
+    #[allow(clippy::trivially_copy_pass_by_ref)]
     pub fn slice_source_code_at(&self, location: &Location) -> Cow<'_, str> {
         let rope = self.get_or_init_rope();
         let offset = rope.line_to_char(location.row() - 1) + location.column();
         Cow::from(rope.slice(offset..))
     }
 
+    #[allow(clippy::trivially_copy_pass_by_ref)]
     pub fn slice_source_code_until(&self, location: &Location) -> Cow<'_, str> {
         let rope = self.get_or_init_rope();
         let offset = rope.line_to_char(location.row() - 1) + location.column();
diff --git a/src/source_code/mod.rs b/src/source_code/mod.rs
index 769fea16c7c51d..e430b6c537dd23 100644
--- a/src/source_code/mod.rs
+++ b/src/source_code/mod.rs
@@ -2,6 +2,18 @@ mod generator;
 mod locator;
 mod stylist;
 
-pub use generator::Generator;
-pub use locator::Locator;
-pub use stylist::{LineEnding, Stylist};
+pub(crate) use generator::Generator;
+pub(crate) use locator::Locator;
+use rustpython_parser::error::ParseError;
+use rustpython_parser::parser;
+pub(crate) use stylist::{LineEnding, Stylist};
+
+/// Run round-trip source code generation on a given Python code.
+pub fn round_trip(code: &str, source_path: &str) -> Result<String, ParseError> {
+    let locator = Locator::new(code);
+    let python_ast = parser::parse_program(code, source_path)?;
+    let stylist = Stylist::from_contents(code, &locator);
+    let mut generator: Generator = (&stylist).into();
+    generator.unparse_suite(&python_ast);
+    Ok(generator.generate())
+}