use proc_macro2::Span;
use std::{
borrow::Cow,
error, fmt, io,
path::{Path, PathBuf},
};
use syn::spanned::Spanned;
use syn::ItemMod;
mod mod_path;
mod resolver;
mod visitor;
pub(crate) use mod_path::*;
pub(crate) use resolver::*;
pub(crate) use visitor::Visitor;
pub fn parse_and_inline_modules(src_file: &Path) -> syn::File {
InlinerBuilder::default()
.parse_and_inline_modules(src_file)
.unwrap()
.output
}
#[derive(Debug)]
pub struct InlinerBuilder {
root: bool,
}
impl Default for InlinerBuilder {
fn default() -> Self {
InlinerBuilder { root: true }
}
}
impl InlinerBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn root(&mut self, root: bool) -> &mut Self {
self.root = root;
self
}
pub fn parse_and_inline_modules(&self, src_file: &Path) -> Result<InliningResult, Error> {
self.parse_internal(src_file, FsResolver::default())
}
fn parse_internal<R: FileResolver + Clone>(
&self,
src_file: &Path,
resolver: R,
) -> Result<InliningResult, Error> {
let mut errors = Some(vec![]);
let result =
Visitor::<R>::with_resolver(src_file, self.root, errors.as_mut(), Cow::Owned(resolver))
.visit()?;
Ok(InliningResult::new(result, errors.unwrap_or_default()))
}
}
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Parse(syn::Error),
}
impl error::Error for Error {}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::Io(err)
}
}
impl From<syn::Error> for Error {
fn from(err: syn::Error) -> Self {
Error::Parse(err)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Io(err) => write!(f, "IO error: {}", err),
Error::Parse(err) => write!(f, "parse error: {}", err),
}
}
}
pub struct InliningResult {
output: syn::File,
errors: Vec<InlineError>,
}
impl InliningResult {
pub(crate) fn new(output: syn::File, errors: Vec<InlineError>) -> Self {
InliningResult { output, errors }
}
pub fn output(&self) -> &syn::File {
&self.output
}
pub fn errors(&self) -> &[InlineError] {
&self.errors
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn into_output_and_errors(self) -> (syn::File, Vec<InlineError>) {
(self.output, self.errors)
}
}
impl fmt::Debug for InliningResult {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.errors.fmt(f)
}
}
impl fmt::Display for InliningResult {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Inlining partially completed before errors:")?;
for error in &self.errors {
writeln!(f, " * {}", error)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct InlineError {
src_path: PathBuf,
module_name: String,
src_span: Span,
path: PathBuf,
kind: Error,
}
impl InlineError {
pub(crate) fn new(
src_path: impl Into<PathBuf>,
item_mod: &ItemMod,
path: impl Into<PathBuf>,
kind: Error,
) -> Self {
Self {
src_path: src_path.into(),
module_name: item_mod.ident.to_string(),
src_span: item_mod.span(),
path: path.into(),
kind,
}
}
pub fn src_path(&self) -> &Path {
&self.src_path
}
pub fn module_name(&self) -> &str {
&self.module_name
}
pub fn src_span(&self) -> proc_macro2::Span {
self.src_span
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn kind(&self) -> &Error {
&self.kind
}
}
impl fmt::Display for InlineError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let start = self.src_span.start();
write!(
f,
"{}:{}:{}: error while including {}: {}",
self.src_path.display(),
start.line,
start.column,
self.path.display(),
self.kind
)
}
}
#[cfg(test)]
mod tests {
use quote::{quote, ToTokens};
use super::*;
fn make_test_env() -> TestResolver {
let mut env = TestResolver::default();
env.register("src/lib.rs", "mod first;");
env.register("src/first/mod.rs", "mod second;");
env.register(
"src/first/second.rs",
r#"
#[doc = " Documentation"]
mod third {
mod fourth;
}
pub fn sample() -> usize { 4 }
"#,
);
env.register(
"src/first/second/third/fourth.rs",
"pub fn another_fn() -> bool { true }",
);
env
}
#[test]
fn happy_path() {
let result = InlinerBuilder::default()
.parse_internal(Path::new("src/lib.rs"), make_test_env())
.unwrap()
.output;
assert_eq!(
result.into_token_stream().to_string(),
quote! {
mod first {
mod second {
#[doc = " Documentation"]
mod third {
mod fourth {
pub fn another_fn() -> bool {
true
}
}
}
pub fn sample() -> usize {
4
}
}
}
}
.to_string()
);
}
#[test]
fn missing_module() {
let mut env = TestResolver::default();
env.register("src/lib.rs", "mod missing;\nmod invalid;");
env.register("src/invalid.rs", "this-is-not-valid-rust!");
let result = InlinerBuilder::default().parse_internal(Path::new("src/lib.rs"), env);
if let Ok(r) = result {
let errors = &r.errors;
assert_eq!(errors.len(), 2, "expected 2 errors");
let error = &errors[0];
assert_eq!(
error.src_path(),
Path::new("src/lib.rs"),
"correct source path"
);
assert_eq!(error.module_name(), "missing");
assert_eq!(error.src_span().start().line, 1);
assert_eq!(error.src_span().start().column, 0);
assert_eq!(error.src_span().end().line, 1);
assert_eq!(error.src_span().end().column, 12);
assert_eq!(error.path(), Path::new("src/missing/mod.rs"));
let io_err = match error.kind() {
Error::Io(err) => err,
_ => panic!("expected ErrorKind::Io, found {}", error.kind()),
};
assert_eq!(io_err.kind(), io::ErrorKind::NotFound);
let error = &errors[1];
assert_eq!(
error.src_path(),
Path::new("src/lib.rs"),
"correct source path"
);
assert_eq!(error.module_name(), "invalid");
assert_eq!(error.src_span().start().line, 2);
assert_eq!(error.src_span().start().column, 0);
assert_eq!(error.src_span().end().line, 2);
assert_eq!(error.src_span().end().column, 12);
assert_eq!(error.path(), Path::new("src/invalid.rs"));
match error.kind() {
Error::Parse(_) => {}
Error::Io(_) => panic!("expected ErrorKind::Parse, found {}", error.kind()),
}
} else {
unreachable!();
}
}
#[test]
#[should_panic]
fn cfg_attrs() {
let mut env = TestResolver::default();
env.register(
"src/lib.rs",
r#"
#[cfg(feature = "m1")]
mod m1;
#[cfg_attr(feature = "m2", path = "m2.rs")]
#[cfg_attr(not(feature = "m2"), path = "empty.rs")]
mod placeholder;
"#,
);
env.register("src/m1.rs", "struct M1;");
env.register(
"src/m2.rs",
"
//! module level doc comment
struct M2;
",
);
env.register("src/empty.rs", "");
let result = InlinerBuilder::default()
.parse_internal(Path::new("src/lib.rs"), env)
.unwrap()
.output;
assert_eq!(
result.into_token_stream().to_string(),
quote! {
#[cfg(feature = "m1")]
mod m1 {
struct M1;
}
#[cfg(feature = "m2")]
mod placeholder {
struct M2;
}
#[cfg(not(feature = "m2"))]
mod placeholder {
}
}
.to_string()
)
}
#[test]
fn cfg_attrs_revised() {
let mut env = TestResolver::default();
env.register(
"src/lib.rs",
r#"
#[cfg(feature = "m1")]
mod m1;
#[cfg(feature = "m2")]
#[path = "m2.rs"]
mod placeholder;
#[cfg(not(feature = "m2"))]
#[path = "empty.rs"]
mod placeholder;
"#,
);
env.register("src/m1.rs", "struct M1;");
env.register(
"src/m2.rs",
r#"
#![doc = " module level doc comment"]
struct M2;
"#,
);
env.register("src/empty.rs", "");
let result = InlinerBuilder::default()
.parse_internal(Path::new("src/lib.rs"), env)
.unwrap()
.output;
assert_eq!(
result.into_token_stream().to_string(),
quote! {
#[cfg(feature = "m1")]
mod m1 {
struct M1;
}
#[cfg(feature = "m2")]
#[path = "m2.rs"]
mod placeholder {
#![doc = " module level doc comment"]
struct M2;
}
#[cfg(not(feature = "m2"))]
#[path = "empty.rs"]
mod placeholder {
}
}
.to_string()
)
}
}