syn_inline_mod/
lib.rs

1//! Utility to traverse the file-system and inline modules that are declared as references to
2//! other Rust files.
3
4use proc_macro2::Span;
5use std::{
6    error, fmt, io,
7    path::{Path, PathBuf},
8};
9use syn::spanned::Spanned;
10use syn::ItemMod;
11
12mod mod_path;
13mod resolver;
14mod visitor;
15
16pub(crate) use mod_path::*;
17pub(crate) use resolver::*;
18pub(crate) use visitor::Visitor;
19
20/// Parse the source code in `src_file` and return a `syn::File` that has all modules
21/// recursively inlined.
22///
23/// This is equivalent to using an `InlinerBuilder` with the default settings.
24///
25/// # Panics
26///
27/// This function will panic if `src_file` cannot be opened or does not contain valid Rust
28/// source code.
29///
30/// # Error Handling
31///
32/// This function ignores most error cases to return a best-effort result. To be informed of
33/// failures that occur while inlining referenced modules, create an `InlinerBuilder` instead.
34pub fn parse_and_inline_modules(src_file: &Path) -> syn::File {
35    InlinerBuilder::default()
36        .parse_and_inline_modules(src_file)
37        .unwrap()
38        .output
39}
40
41/// A builder that can configure how to inline modules.
42///
43/// After creating a builder, set configuration options using the methods
44/// taking `&mut self`, then parse and inline one or more files using
45/// `parse_and_inline_modules`.
46#[derive(Debug)]
47pub struct InlinerBuilder {
48    root: bool,
49}
50
51impl Default for InlinerBuilder {
52    fn default() -> Self {
53        InlinerBuilder { root: true }
54    }
55}
56
57impl InlinerBuilder {
58    /// Create a new `InlinerBuilder` with the default options.
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Configures whether the module being parsed is a root module or not.
64    ///
65    /// A root module is one that is passed directly to `rustc`. A non-root
66    /// module is one that is included from another module using a `mod` item.
67    ///
68    /// Default: `true`.
69    pub fn root(&mut self, root: bool) -> &mut Self {
70        self.root = root;
71        self
72    }
73
74    /// Parse the source code in `src_file` and return an `InliningResult` that has all modules
75    /// recursively inlined.
76    pub fn parse_and_inline_modules(&self, src_file: &Path) -> Result<InliningResult, Error> {
77        self.parse_internal(src_file, &mut FsResolver::new(|_: &Path, _| {}))
78    }
79
80    /// Parse the source code in `src_file` and return an `InliningResult` that has all modules
81    /// recursively inlined. Call the given callback whenever a file is loaded from disk (regardless
82    /// of if it parsed successfully).
83    pub fn inline_with_callback(
84        &self,
85        src_file: &Path,
86        on_load: impl FnMut(&Path, String),
87    ) -> Result<InliningResult, Error> {
88        self.parse_internal(src_file, &mut FsResolver::new(on_load))
89    }
90
91    fn parse_internal<R: FileResolver>(
92        &self,
93        src_file: &Path,
94        resolver: &mut R,
95    ) -> Result<InliningResult, Error> {
96        // XXX There is no way for library callers to disable error tracking,
97        // but until we're sure that there's no performance impact of enabling it
98        // we'll let downstream code think that error tracking is optional.
99        let mut errors = Some(vec![]);
100        let result = Visitor::<R>::new(src_file, self.root, errors.as_mut(), resolver).visit()?;
101        Ok(InliningResult::new(result, errors.unwrap_or_default()))
102    }
103}
104
105/// An error that was encountered while reading, parsing or inlining a module.
106///
107/// Errors block further progress on inlining, but do not invalidate other progress.
108/// Therefore, only an error on the initially-passed-in-file is fatal to inlining.
109#[derive(Debug)]
110pub enum Error {
111    /// An error happened while opening or reading the file.
112    Io(io::Error),
113
114    /// Errors happened while using `syn` to parse the file.
115    Parse(syn::Error),
116}
117
118impl error::Error for Error {
119    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
120        match self {
121            Error::Io(err) => Some(err),
122            Error::Parse(err) => Some(err),
123        }
124    }
125}
126
127impl From<io::Error> for Error {
128    fn from(err: io::Error) -> Self {
129        Error::Io(err)
130    }
131}
132
133impl From<syn::Error> for Error {
134    fn from(err: syn::Error) -> Self {
135        Error::Parse(err)
136    }
137}
138
139impl fmt::Display for Error {
140    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141        match self {
142            Error::Io(_) => write!(f, "IO error"),
143            Error::Parse(_) => write!(f, "parse error"),
144        }
145    }
146}
147
148/// The result of a best-effort attempt at inlining.
149///
150/// This struct guarantees that the origin file was readable and valid Rust source code, but
151/// `errors` must be inspected to check if everything was inlined successfully.
152pub struct InliningResult {
153    output: syn::File,
154    errors: Vec<InlineError>,
155}
156
157impl InliningResult {
158    /// Create a new `InliningResult` with the best-effort output and any errors encountered
159    /// during the inlining process.
160    pub(crate) fn new(output: syn::File, errors: Vec<InlineError>) -> Self {
161        InliningResult { output, errors }
162    }
163
164    /// The best-effort result of inlining.
165    pub fn output(&self) -> &syn::File {
166        &self.output
167    }
168
169    /// The errors that kept the inlining from completing. May be empty if there were no errors.
170    pub fn errors(&self) -> &[InlineError] {
171        &self.errors
172    }
173
174    /// Whether the result has any errors. `false` implies that all inlining operations completed
175    /// successfully.
176    pub fn has_errors(&self) -> bool {
177        !self.errors.is_empty()
178    }
179
180    /// Break an incomplete inlining into the best-effort parsed result and the errors encountered.
181    ///
182    /// # Usage
183    ///
184    /// ```rust,ignore
185    /// # #![allow(unused_variables)]
186    /// # use std::path::Path;
187    /// # use syn_inline_mod::InlinerBuilder;
188    /// let result = InlinerBuilder::default().parse_and_inline_modules(Path::new("foo.rs"));
189    /// match result {
190    ///     Err(e) => unimplemented!(),
191    ///     Ok(r) if r.has_errors() => {
192    ///         let (best_effort, errors) = r.into_output_and_errors();
193    ///         // do things with the partial output and the errors
194    ///     },
195    ///     Ok(r) => {
196    ///         let (complete, _) = r.into_output_and_errors();
197    ///         // do things with the completed output
198    ///     }
199    /// }
200    /// ```
201    pub fn into_output_and_errors(self) -> (syn::File, Vec<InlineError>) {
202        (self.output, self.errors)
203    }
204}
205
206impl fmt::Debug for InliningResult {
207    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208        self.errors.fmt(f)
209    }
210}
211
212impl fmt::Display for InliningResult {
213    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
214        writeln!(f, "Inlining partially completed before errors:")?;
215        for error in &self.errors {
216            writeln!(f, " * {}", error)?;
217        }
218
219        Ok(())
220    }
221}
222
223/// An error that happened while attempting to inline a module.
224#[derive(Debug)]
225pub struct InlineError {
226    src_path: PathBuf,
227    module_name: String,
228    src_span: Span,
229    path: PathBuf,
230    kind: Error,
231}
232
233impl InlineError {
234    pub(crate) fn new(
235        src_path: impl Into<PathBuf>,
236        item_mod: &ItemMod,
237        path: impl Into<PathBuf>,
238        kind: Error,
239    ) -> Self {
240        Self {
241            src_path: src_path.into(),
242            module_name: item_mod.ident.to_string(),
243            src_span: item_mod.span(),
244            path: path.into(),
245            kind,
246        }
247    }
248
249    /// Returns the source path where the error originated.
250    ///
251    /// The file at this path parsed correctly, but it caused the file at `self.path()` to be read.
252    pub fn src_path(&self) -> &Path {
253        &self.src_path
254    }
255
256    /// Returns the name of the module that was attempted to be inlined.
257    pub fn module_name(&self) -> &str {
258        &self.module_name
259    }
260
261    /// Returns the `Span` (including line and column information) in the source path that caused
262    /// `self.path()` to be included.
263    pub fn src_span(&self) -> proc_macro2::Span {
264        self.src_span
265    }
266
267    /// Returns the path where the error happened.
268    ///
269    /// Reading and parsing this file failed for the reason listed in `self.kind()`.
270    pub fn path(&self) -> &Path {
271        &self.path
272    }
273
274    /// Returns the reason for this error happening.
275    pub fn kind(&self) -> &Error {
276        &self.kind
277    }
278}
279
280impl fmt::Display for InlineError {
281    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282        let start = self.src_span.start();
283        write!(
284            f,
285            "{}:{}:{}: error while including {}: {}",
286            self.src_path.display(),
287            start.line,
288            start.column,
289            self.path.display(),
290            self.kind
291        )
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use quote::{quote, ToTokens};
298
299    use super::*;
300
301    fn make_test_env() -> TestResolver {
302        let mut env = TestResolver::default();
303        env.register("src/lib.rs", "mod first;");
304        env.register("src/first/mod.rs", "mod second;");
305        env.register(
306            "src/first/second.rs",
307            r#"
308            #[doc = " Documentation"]
309            mod third {
310                mod fourth;
311            }
312
313            pub fn sample() -> usize { 4 }
314            "#,
315        );
316        env.register(
317            "src/first/second/third/fourth.rs",
318            "pub fn another_fn() -> bool { true }",
319        );
320        env
321    }
322
323    /// Run a full test, exercising the entirety of the functionality in this crate.
324    #[test]
325    fn happy_path() {
326        let result = InlinerBuilder::default()
327            .parse_internal(Path::new("src/lib.rs"), &mut make_test_env())
328            .unwrap()
329            .output;
330
331        assert_eq!(
332            result.into_token_stream().to_string(),
333            quote! {
334                mod first {
335                    mod second {
336                        #[doc = " Documentation"]
337                        mod third {
338                            mod fourth {
339                                pub fn another_fn() -> bool {
340                                    true
341                                }
342                            }
343                        }
344
345                        pub fn sample() -> usize {
346                            4
347                        }
348                    }
349                }
350            }
351            .to_string()
352        );
353    }
354
355    /// Test case involving missing and invalid modules
356    #[test]
357    fn missing_module() {
358        let mut env = TestResolver::default();
359        env.register("src/lib.rs", "mod missing;\nmod invalid;");
360        env.register("src/invalid.rs", "this-is-not-valid-rust!");
361
362        let result = InlinerBuilder::default().parse_internal(Path::new("src/lib.rs"), &mut env);
363
364        if let Ok(r) = result {
365            let errors = &r.errors;
366            assert_eq!(errors.len(), 2, "expected 2 errors");
367
368            let error = &errors[0];
369            assert_eq!(
370                error.src_path(),
371                Path::new("src/lib.rs"),
372                "correct source path"
373            );
374            assert_eq!(error.module_name(), "missing");
375            assert_eq!(error.src_span().start().line, 1);
376            assert_eq!(error.src_span().start().column, 0);
377            assert_eq!(error.src_span().end().line, 1);
378            assert_eq!(error.src_span().end().column, 12);
379            assert_eq!(error.path(), Path::new("src/missing/mod.rs"));
380            let io_err = match error.kind() {
381                Error::Io(err) => err,
382                _ => panic!("expected ErrorKind::Io, found {}", error.kind()),
383            };
384            assert_eq!(io_err.kind(), io::ErrorKind::NotFound);
385
386            let error = &errors[1];
387            assert_eq!(
388                error.src_path(),
389                Path::new("src/lib.rs"),
390                "correct source path"
391            );
392            assert_eq!(error.module_name(), "invalid");
393            assert_eq!(error.src_span().start().line, 2);
394            assert_eq!(error.src_span().start().column, 0);
395            assert_eq!(error.src_span().end().line, 2);
396            assert_eq!(error.src_span().end().column, 12);
397            assert_eq!(error.path(), Path::new("src/invalid.rs"));
398            match error.kind() {
399                Error::Parse(_) => {}
400                Error::Io(_) => panic!("expected ErrorKind::Parse, found {}", error.kind()),
401            }
402        } else {
403            unreachable!();
404        }
405    }
406
407    /// Test case involving `cfg_attr` from the original request for implementation.
408    ///
409    /// Right now, this test fails for two reasons:
410    ///
411    /// 1. We don't look for `cfg_attr` elements
412    /// 2. We don't have a way to insert new items
413    ///
414    /// The first fix is simpler, but the second one would be difficult.
415    #[test]
416    #[should_panic]
417    fn cfg_attrs() {
418        let mut env = TestResolver::default();
419        env.register(
420            "src/lib.rs",
421            r#"
422            #[cfg(feature = "m1")]
423            mod m1;
424
425            #[cfg_attr(feature = "m2", path = "m2.rs")]
426            #[cfg_attr(not(feature = "m2"), path = "empty.rs")]
427            mod placeholder;
428        "#,
429        );
430        env.register("src/m1.rs", "struct M1;");
431        env.register(
432            "src/m2.rs",
433            "
434        //! module level doc comment
435
436        struct M2;
437        ",
438        );
439        env.register("src/empty.rs", "");
440
441        let result = InlinerBuilder::default()
442            .parse_internal(Path::new("src/lib.rs"), &mut env)
443            .unwrap()
444            .output;
445
446        assert_eq!(
447            result.into_token_stream().to_string(),
448            quote! {
449                #[cfg(feature = "m1")]
450                mod m1 {
451                    struct M1;
452                }
453
454                #[cfg(feature = "m2")]
455                mod placeholder {
456                    //! module level doc comment
457
458                    struct M2;
459                }
460
461                #[cfg(not(feature = "m2"))]
462                mod placeholder {
463
464                }
465            }
466            .to_string()
467        )
468    }
469
470    #[test]
471    fn cfg_attrs_revised() {
472        let mut env = TestResolver::default();
473        env.register(
474            "src/lib.rs",
475            r#"
476            #[cfg(feature = "m1")]
477            mod m1;
478
479            #[cfg(feature = "m2")]
480            #[path = "m2.rs"]
481            mod placeholder;
482
483            #[cfg(not(feature = "m2"))]
484            #[path = "empty.rs"]
485            mod placeholder;
486        "#,
487        );
488        env.register("src/m1.rs", "struct M1;");
489        env.register(
490            "src/m2.rs",
491            r#"
492            #![doc = " module level doc comment"]
493
494            struct M2;
495            "#,
496        );
497        env.register("src/empty.rs", "");
498
499        let result = InlinerBuilder::default()
500            .parse_internal(Path::new("src/lib.rs"), &mut env)
501            .unwrap()
502            .output;
503
504        assert_eq!(
505            result.into_token_stream().to_string(),
506            quote! {
507                #[cfg(feature = "m1")]
508                mod m1 {
509                    struct M1;
510                }
511
512                #[cfg(feature = "m2")]
513                #[path = "m2.rs"]
514                mod placeholder {
515                    #![doc = " module level doc comment"]
516
517                    struct M2;
518                }
519
520                #[cfg(not(feature = "m2"))]
521                #[path = "empty.rs"]
522                mod placeholder {
523
524                }
525            }
526            .to_string()
527        )
528    }
529}