Skip to main content

docstr/
lib.rs

1#![doc = concat!("[![crates.io](https://img.shields.io/crates/v/", env!("CARGO_PKG_NAME"), "?style=flat-square&logo=rust)](https://crates.io/crates/", env!("CARGO_PKG_NAME"), ")")]
2#![doc = concat!("[![docs.rs](https://img.shields.io/docsrs/", env!("CARGO_PKG_NAME"), "?style=flat-square&logo=docs.rs)](https://docs.rs/", env!("CARGO_PKG_NAME"), ")")]
3#![doc = "![license](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue?style=flat-square)"]
4#![doc = concat!("![msrv](https://img.shields.io/badge/msrv-", env!("CARGO_PKG_RUST_VERSION"), "-blue?style=flat-square&logo=rust)")]
5//! [![github](https://img.shields.io/github/stars/nik-rev/docstr)](https://github.com/nik-rev/docstr)
6//!
7//! This crate provides a macro [`docstr!`] for ergonomically creating multi-line string literals.
8//!
9//! ```toml
10#![doc = concat!(env!("CARGO_PKG_NAME"), " = ", "\"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"), "\"")]
11//! ```
12//!
13//! Note: `docstr` does not have any dependencies such as `syn` or `quote`, so compile-speeds are very fast.
14//!
15//! # Usage
16//!
17//! [`docstr!`](crate::docstr) takes documentation comments as arguments and converts them into a string
18//!
19//! ```rust
20//! use docstr::docstr;
21//!
22//! let hello_world_in_c: &'static str = docstr!(
23//!     /// #include <stdio.h>
24//!     ///
25//!     /// int main(int argc, char **argv) {
26//!     ///     printf("hello world\n");
27//!     ///     return 0;
28//!     /// }
29//! );
30//!
31//! assert_eq!(hello_world_in_c, r#"#include <stdio.h>
32//!
33//! int main(int argc, char **argv) {
34//!     printf("hello world\n");
35//!     return 0;
36//! }"#)
37//! ```
38//!
39//! # Composition
40//!
41//! [`docstr!`](crate::docstr) can pass the generated string to any macro. This example shows the string being forwarded to the [`format!`] macro:
42//!
43//! ```
44//! # use docstr::docstr;
45//! let name = "Bob";
46//! let age = 21;
47//!
48//! let greeting: String = docstr!(format!
49//!     /// Hello, my name is {name}.
50//!     /// I am {} years old!
51//!     age
52//! );
53//!
54//! assert_eq!(greeting, "\
55//! Hello, my name is Bob.
56//! I am 21 years old!");
57//! ```
58//!
59//! This is great because there's just a single macro, `docstr!`, that can do anything. No need for `docstr_format!`, `docstr_println!`, `docstr_write!`, etc.
60//!
61//! ## How composition works
62//!
63//! If the first argument to `docstr!` is a path to a macro, that macro will be called. This invocation:
64//!
65//! ```
66//! # use docstr::docstr;
67//! let greeting: String = docstr!(format!
68//!     /// Hello, my name is {name}.
69//!     /// I am {} years old!
70//!     age
71//! );
72//! ```
73//!
74//! Is equivalent to this:
75//!
76//! ```
77//! let greeting: String = format!("\
78//! Hello, my name is {name}.
79//! I am {} years old!"
80//!     age,
81//! );
82//! ```
83//!
84//! You can inject arguments before the format string:
85//!
86//! ```rust
87//! # let mut w = String::new();
88//! # use std::fmt::Write as _;
89//! # use docstr::docstr;
90//! docstr!(write! w
91//!    /// Hello, world!
92//! );
93//! ```
94//!
95//! Expands to:
96//!
97//! ```rust
98//! # let mut w = String::new();
99//! # use std::fmt::Write as _;
100//! write!(w, "Hello, world!");
101//! ```
102//!
103//! # Global Import
104//!
105//! This will make `docstr!` globally accessible in your entire crate, without needing to import it:
106//!
107//! ```
108//! #[macro_use(docstr)]
109//! extern crate docstr;
110//! ```
111
112use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
113
114/// Turns documentation comments into string at compile-time.
115///
116/// ```rust
117/// use docstr::docstr;
118///
119/// let hello_world: String = docstr!(format!
120///     /// fn say_hi() {{
121///     ///     println!("Hello, my name is {}");
122///     /// }}
123///     "Bob"
124/// );
125///
126/// assert_eq!(hello_world, r#"fn say_hi() {
127///     println!("Hello, my name is Bob");
128/// }"#);
129/// ```
130///
131/// Expands to this:
132///
133/// ```rust
134/// format!(r#"fn say_hi() {{
135///     println!("Hello, my name is {}");
136/// }}"#, "Bob");
137/// ```
138///
139/// See the [crate-level](crate) documentation for more info
140#[proc_macro]
141pub fn docstr(input: TokenStream) -> TokenStream {
142    let mut input = input.into_iter().peekable();
143
144    // If we encounter any errors, we collect them into here
145    // and report them all at once
146    //
147    // compile_error!("you have done horrible things!")
148    let mut compile_errors = TokenStream::new();
149    let mut compile_error = |span: Span, message: &str| {
150        compile_errors.extend(CompileError::new(span, message));
151    };
152
153    // Path to the macro that we send tokens to.
154    //
155    // If this is `None`, we don't forward the path to any macro,
156    // and docstr! produces a string literal of type &'static str
157    //
158    // docstr!(
159    //     /// hello world
160    // )
161    // => "hello world"
162    //
163    // docstr!(format!
164    //     /// hello {world}
165    // )
166    // => format!("hello {world}")
167    let macro_path = match input.peek() {
168        // No macro path, this will directly produce a string literal
169        //
170        // docstr!(
171        //     /// hello world
172        // )
173        Some(TokenTree::Punct(punct)) if *punct == '#' => None,
174        // Macro input is completely empty
175        //
176        // docstr!()
177        None => {
178            return CompileError::new(
179                Span::call_site(),
180                "requires at least a documentation comment argument: `/// ...`",
181            )
182            .into()
183        }
184        // Path to a macro.
185        //
186        // docstr!(format!
187        //     /// hello {world}
188        // )
189        Some(_) => {
190            // Contains tokens of the macro, e.g. `std::format!`
191            match extract_macro_path(&mut input) {
192                Ok(macro_path) => macro_path,
193                Err(compile_error) => return compile_error.into(),
194            }
195        }
196    };
197
198    // Tokens BEFORE the doc comments, which are appended
199    // directly to the `macro_path` we just got - before the `doc_comments`
200    let mut tokens_before_doc_comments = TokenStream::new();
201
202    // Contents of the doc comments which we collect
203    //
204    // /// foo
205    // /// bar
206    //
207    // Expands to:
208    //
209    // #[doc = "foo"]
210    // #[doc = "bar"]
211    //
212    // Which we collect to:
213    //
214    // ["foo", "bar"]
215    let mut doc_comments = Vec::new();
216
217    // Tokens AFTER the doc comments, which are appended
218    // directly to the `macro_path` we just got - after the `doc_comments`
219    let mut tokens_after_doc_comments = TokenStream::new();
220
221    /// In the middle of `docstr!(...)` macro's invocation, we will always have doc comments.
222    ///
223    /// ```ignore
224    /// docstr!(
225    ///     // DocComments::NotReached
226    ///     but we can have tokens here
227    ///     // DocComments::Inside
228    ///     /// foo
229    ///     /// bar
230    ///     // DocComments::Finished
231    ///     and here too
232    /// )
233    /// ```
234    #[derive(Eq, PartialEq, PartialOrd, Ord)]
235    enum DocCommentProgress {
236        /// doc comments `///` not reached yet
237        NotReached,
238        /// currently we are INSIDE the doc comments
239        Inside,
240        /// We have parsed all the doc comments
241        Finished,
242    }
243
244    // State machine corresponding to our current progress in the macro
245    let mut doc_comment_progress = DocCommentProgress::NotReached;
246
247    // Let's collect all of the doc comments into a Vec<String> where each
248    // String corresponds to the doc comment
249    while let Some(tt) = input.next() {
250        // #[doc = "..."]
251        // ^
252        let doc_comment_start_span = match tt {
253            // this token is passed verbatim to the macro at the end,
254            // after the doc comments
255            tt if doc_comment_progress == DocCommentProgress::Finished => {
256                tokens_after_doc_comments.extend([tt]);
257                continue;
258            }
259            // start of doc comment
260            TokenTree::Punct(punct) if punct == '#' => {
261                match doc_comment_progress {
262                    DocCommentProgress::NotReached => {
263                        doc_comment_progress = DocCommentProgress::Inside;
264                    }
265                    DocCommentProgress::Inside => {
266                        // ok
267                    }
268                    DocCommentProgress::Finished => {
269                        unreachable!("if it's finished we would `continue` in an earlier arm")
270                    }
271                }
272                match input.peek() {
273                    Some(TokenTree::Punct(punct)) if *punct == '!' => {
274                        compile_error(
275                            punct.span(),
276                            "Inner doc comments `//! ...` are not supported. Please use `/// ...`",
277                        );
278                        // eat '!'
279                        input.next();
280                    }
281                    _ => (),
282                }
283                punct.span()
284            }
285            // this token is passed verbatim to the macro at the beginning,
286            // before the doc comments
287            tt if doc_comment_progress == DocCommentProgress::NotReached => {
288                // Comma before '#' is optional
289                //
290                // docstr!(writeln! w,
291                //                   ^ this comma can be omitted
292                //     #[doc = "..."]
293                //     ^ next token
294                // )
295                let insert_comma = match input.peek() {
296                    Some(TokenTree::Punct(next)) => match &tt {
297                        TokenTree::Punct(current) if *current == ',' && *next == '#' => false,
298                        _ if *next == '#' => true,
299                        _ => false,
300                    },
301                    _ => false,
302                };
303
304                tokens_before_doc_comments.extend([tt]);
305
306                if insert_comma {
307                    tokens_before_doc_comments
308                        .extend([TokenTree::Punct(Punct::new(',', Spacing::Joint))]);
309                }
310
311                continue;
312            }
313            _ => {
314                unreachable!("when the next token is not `#` progress is `Finished`")
315            }
316        };
317
318        // #[doc = "..."]
319        //  ^^^^^^^^^^^^^
320        let doc_comment_square_brackets = match input.next() {
321            Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
322            Some(tt) => {
323                compile_error(tt.span(), "expected `[...]`");
324                continue;
325            }
326            None => {
327                compile_error(
328                    doc_comment_start_span,
329                    "expected `#` to be followed by `[...]`",
330                );
331                continue;
332            }
333        };
334
335        // Check if there is a doc comment after this one
336        //
337        // #[doc = "..."]            #[doc = "..."]
338        // ^^^^^^^^^^^^^^ current    ^ next?
339        match input.peek() {
340            Some(TokenTree::Punct(punct)) if *punct == '#' => {
341                // Yes, there is. Continue doc comment
342            }
343            _ => {
344                // The next token is not `#` so there are no more doc comments
345                doc_comment_progress = DocCommentProgress::Finished;
346            }
347        }
348
349        // #[doc = "..."]
350        //  ^^^^^^^^^^^^^
351        let mut doc_comment_attribute_inner = doc_comment_square_brackets.stream().into_iter();
352
353        // #[doc = "..."]
354        //   ^^^
355        let kw_doc_span = match doc_comment_attribute_inner.next() {
356            Some(TokenTree::Ident(kw_doc)) if kw_doc.to_string() == "doc" => kw_doc.span(),
357            Some(tt) => {
358                compile_error(tt.span(), "expected `doc`");
359                continue;
360            }
361            None => {
362                compile_error(
363                    doc_comment_square_brackets.span_open(),
364                    "expected `doc` after `[`",
365                );
366                continue;
367            }
368        };
369
370        // #[doc = "..."]
371        //       ^
372        let punct_eq_span = match doc_comment_attribute_inner.next() {
373            Some(TokenTree::Punct(eq)) if eq == '=' => eq.span(),
374            Some(tt) => {
375                compile_error(tt.span(), "expected `=`");
376                continue;
377            }
378            None => {
379                compile_error(kw_doc_span, "expected `=` after `doc`");
380                continue;
381            }
382        };
383
384        // #[doc = "..."]
385        //         ^^^^^
386        let next = doc_comment_attribute_inner.next();
387        let Some(tt) = next else {
388            compile_error(punct_eq_span, "expected string literal after `=`");
389            continue;
390        };
391        let span = tt.span();
392
393        // #[doc = "..."]
394        //          ^^^
395        let Ok(litrs::Literal::String(literal)) = litrs::Literal::try_from(tt) else {
396            compile_error(
397                span,
398                "only string \"...\" or r\"...\" literals are supported",
399            );
400            continue;
401        };
402
403        let literal = literal.value();
404
405        // Reached contents of the doc comment
406        //
407        // let's remove leading space
408        //
409        // /// foo bar
410        //
411        // this expands to:
412        //
413        // #[doc = " foo bar"]
414        //          ^ remove this space from the actual output
415        //
416        // We usually always have a space after the comment token,
417        // since it looks good. And e.g. Rustdoc ignores it as well.
418        let literal = literal.strip_prefix(' ').unwrap_or(literal);
419
420        doc_comments.push(literal.to_string());
421    }
422
423    if doc_comments.is_empty() {
424        compile_error(
425            Span::call_site(),
426            "requires at least a documentation comment argument: `/// ...`",
427        );
428    }
429
430    // The fully constructed string literal that we output
431    //
432    // docstr!(
433    //     /// foo
434    //     /// bar
435    // )
436    //
437    // becomes this:
438    //
439    // "foo\nbar"
440    let string = doc_comments
441        .into_iter()
442        .reduce(|mut acc, s| {
443            acc.push('\n');
444            acc.push_str(&s);
445            acc
446        })
447        .unwrap_or_default();
448
449    let Some(macro_) = macro_path else {
450        if !tokens_before_doc_comments.is_empty() || !tokens_after_doc_comments.is_empty() {
451            compile_error(
452                Span::call_site(),
453                concat!(
454                    "expected macro input to only contain doc comments: `/// ...`, ",
455                    "because you haven't supplied a macro path as the 1st argument"
456                ),
457            );
458        }
459
460        if !compile_errors.is_empty() {
461            return compile_errors;
462        }
463
464        // Just a plain string literal
465        return TokenTree::Literal(Literal::string(&string)).into();
466    };
467
468    if !compile_errors.is_empty() {
469        return compile_errors;
470    }
471
472    // The following:
473    //
474    // let a = docstr!(format!
475    //     hello
476    //     /// foo
477    //     /// bar
478    //     a,
479    //     b
480    // );
481    //
482    // Expands into this:
483    //
484    // let a = format!(hello, "foo\nbar", a, b);
485    TokenStream::from_iter(
486        // format!(hello, "foo\nbar", a, b)
487        // ^^^^^^^
488        macro_.into_iter().chain([TokenTree::Group(Group::new(
489            // format!(hello, "foo\nbar", a, b)
490            //        ^                      ^
491            Delimiter::Parenthesis,
492            // format!(hello, "foo\nbar", a, b)
493            //         ^^^^^^^^^^^^^^^^^^^^^^^
494            TokenStream::from_iter(
495                // format!(hello, "foo\nbar", a, b)
496                //         ^^^^^^
497                tokens_before_doc_comments
498                    .into_iter()
499                    .chain([
500                        // format!(hello, "foo\nbar", a, b)
501                        //                ^^^^^^^^^^
502                        TokenTree::Literal(Literal::string(&string)),
503                        // format!(hello, "foo\nbar", a, b)
504                        //                          ^
505                        TokenTree::Punct(Punct::new(',', Spacing::Joint)),
506                    ])
507                    // format!(hello, "foo\nbar", a, b)
508                    //                            ^^^^
509                    .chain(tokens_after_doc_comments),
510            ),
511        ))]),
512    )
513}
514
515/// Extracts path to macro, if one exists
516///
517/// ```ignore
518/// docstr!(::std::format!
519///         ^^^^^^^^^^^^^^
520///     /// ...
521/// )
522/// ```
523fn extract_macro_path(
524    input: &mut std::iter::Peekable<proc_macro::token_stream::IntoIter>,
525) -> Result<Option<TokenStream>, CompileError> {
526    let mut macro_path = TokenStream::new();
527
528    enum PreviousMacroPathToken {
529        PathSeparator,
530        Ident,
531    }
532
533    // Tracked for better error messages
534    let mut previous_macro_path_token = None;
535
536    macro_rules! invalid_macro_path {
537        () => {
538            CompileError::new(
539                macro_path
540                    .into_iter()
541                    .next()
542                    .map(|tt| tt.span())
543                    .unwrap_or_else(Span::call_site),
544                "invalid macro path",
545            )
546        };
547    }
548
549    // on the first compile error we stop trying to process the path because it won't
550    // make any sense after that
551    loop {
552        let tt = input.next();
553        match tt {
554            // Reached end of macro
555            //
556            // std::format!
557            //            ^
558            Some(TokenTree::Punct(exclamation)) if exclamation == '!' => {
559                macro_path.extend([TokenTree::Punct(exclamation)]);
560                break;
561            }
562            // std::format!
563            //    ^^
564            Some(TokenTree::Punct(colon)) if colon == ':' => {
565                match previous_macro_path_token {
566                    Some(PreviousMacroPathToken::Ident) | None => {
567                        previous_macro_path_token = Some(PreviousMacroPathToken::PathSeparator);
568                    }
569                    Some(PreviousMacroPathToken::PathSeparator) => {
570                        return Err(invalid_macro_path!());
571                    }
572                }
573
574                macro_path.extend([TokenTree::Punct(colon)]);
575
576                match input.next() {
577                    // std::format!
578                    //     ^
579                    Some(TokenTree::Punct(colon)) if colon == ':' => {
580                        macro_path.extend([TokenTree::Punct(colon)]);
581                    }
582                    _ => {
583                        return Err(invalid_macro_path!());
584                    }
585                }
586            }
587            // std::format!
588            // ^^^
589            //      ^^^^^^
590            Some(TokenTree::Ident(ident)) => match previous_macro_path_token {
591                Some(PreviousMacroPathToken::PathSeparator) | None => {
592                    macro_path.extend([TokenTree::Ident(ident)]);
593                    previous_macro_path_token = Some(PreviousMacroPathToken::Ident);
594                }
595                Some(PreviousMacroPathToken::Ident) => {
596                    return Err(invalid_macro_path!());
597                }
598            },
599            _ if !macro_path.is_empty() => {
600                let macro_path_display = macro_path.to_string();
601                let last_token = macro_path.into_iter().last().expect("!.is_empty()");
602                return Err(CompileError::new(
603                    last_token.span(),
604                    format!("macro path must be followed by `!`, try: `{macro_path_display}!`"),
605                ));
606            }
607            _ => {
608                return Err(CompileError::new(
609                    tt.map(|tt| tt.span()).unwrap_or_else(Span::call_site),
610                    "unexpected token",
611                ));
612            }
613        }
614    }
615
616    Ok(Some(macro_path))
617}
618
619/// `.into_iter()` generates `compile_error!($message)` at `$span`
620struct CompileError {
621    /// Where the compile error is generates
622    pub span: Span,
623    /// Message of the compile error
624    pub message: String,
625}
626
627impl From<CompileError> for TokenStream {
628    fn from(value: CompileError) -> Self {
629        value.into_iter().collect()
630    }
631}
632
633impl CompileError {
634    /// Create a new compile error
635    pub fn new(span: Span, message: impl AsRef<str>) -> Self {
636        Self {
637            span,
638            message: message.as_ref().to_string(),
639        }
640    }
641}
642
643impl IntoIterator for CompileError {
644    type Item = TokenTree;
645    type IntoIter = std::array::IntoIter<Self::Item, 3>;
646
647    fn into_iter(self) -> Self::IntoIter {
648        [
649            TokenTree::Ident(Ident::new("compile_error", self.span)),
650            TokenTree::Punct({
651                let mut punct = Punct::new('!', Spacing::Alone);
652                punct.set_span(self.span);
653                punct
654            }),
655            TokenTree::Group({
656                let mut group = Group::new(Delimiter::Brace, {
657                    TokenStream::from_iter(vec![TokenTree::Literal({
658                        let mut string = Literal::string(&self.message);
659                        string.set_span(self.span);
660                        string
661                    })])
662                });
663                group.set_span(self.span);
664                group
665            }),
666        ]
667        .into_iter()
668    }
669}