Skip to main content

wdl_format/token/
post.rs

1//! Postprocessed tokens.
2//!
3//! Generally speaking, unless you are working with the internals of code
4//! formatting, you're not going to be working with these.
5
6use std::collections::HashMap;
7use std::fmt::Display;
8use std::rc::Rc;
9
10use wdl_ast::DIRECTIVE_COMMENT_PREFIX;
11use wdl_ast::DIRECTIVE_DELIMITER;
12use wdl_ast::DOC_COMMENT_PREFIX;
13use wdl_ast::Directive;
14use wdl_ast::SyntaxKind;
15
16use crate::Comment;
17use crate::Config;
18use crate::Indent;
19use crate::PreToken;
20use crate::SPACE;
21use crate::Token;
22use crate::TokenStream;
23use crate::Trivia;
24use crate::TriviaBlankLineSpacingPolicy;
25
26/// [`PostToken`]s that precede an inline comment.
27const INLINE_COMMENT_PRECEDING_TOKENS: [PostToken; 2] = [PostToken::Space, PostToken::Space];
28
29/// A postprocessed token.
30#[derive(Clone, Eq, PartialEq)]
31pub enum PostToken {
32    /// A space.
33    Space,
34
35    /// A newline.
36    Newline,
37
38    /// One indentation.
39    Indent,
40
41    /// A temporary indent.
42    ///
43    /// This is added after a [`PostToken::Indent`] during the formatting of
44    /// command sections.
45    TempIndent(Rc<String>),
46
47    /// A string literal.
48    Literal(Rc<String>),
49
50    /// A doc comment block.
51    Documentation {
52        /// The current indent level.
53        num_indents: usize,
54        /// The contents of the doc comment block.
55        contents: Rc<String>,
56    },
57
58    /// A directive comment.
59    Directive {
60        /// The current indent level.
61        num_indents: usize,
62        /// The directive.
63        directive: Rc<Directive>,
64    },
65}
66
67impl std::fmt::Debug for PostToken {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Self::Space => write!(f, "<SPACE>"),
71            Self::Newline => write!(f, "<NEWLINE>"),
72            Self::Indent => write!(f, "<INDENT>"),
73            Self::TempIndent(value) => write!(f, "<TEMP_INDENT@{value}>"),
74            Self::Literal(value) => write!(f, "<LITERAL@{value}>"),
75            Self::Directive { directive, .. } => write!(f, "<DIRECTIVE@{directive:?}>"),
76            Self::Documentation { contents, .. } => write!(f, "<DOCUMENTATION@{contents}>"),
77        }
78    }
79}
80
81impl Token for PostToken {
82    /// Returns a displayable version of the token.
83    fn display<'a>(&'a self, config: &'a Config) -> impl Display + 'a {
84        /// A displayable version of a [`PostToken`].
85        struct Display<'a> {
86            /// The token to display.
87            token: &'a PostToken,
88            /// The configuration to use.
89            config: &'a Config,
90        }
91
92        fn write_indents(
93            f: &mut std::fmt::Formatter<'_>,
94            indent: &Indent,
95            num_indents: usize,
96        ) -> std::fmt::Result {
97            for _ in 0usize..num_indents {
98                write!(f, "{indent}")?;
99            }
100            Ok(())
101        }
102
103        impl std::fmt::Display for Display<'_> {
104            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105                match self.token {
106                    PostToken::Space => write!(f, "{SPACE}"),
107                    PostToken::Newline => write!(f, "{}", self.config.newline_style.as_str()),
108                    PostToken::Indent => {
109                        write!(f, "{indent}", indent = self.config.indent)
110                    }
111                    PostToken::TempIndent(value) => write!(f, "{value}"),
112                    PostToken::Literal(value) => write!(f, "{value}"),
113                    PostToken::Documentation {
114                        num_indents,
115                        contents: markdown,
116                    } => {
117                        let prefix = DOC_COMMENT_PREFIX;
118                        write!(f, "{prefix}")?;
119                        let mut lines = markdown.lines().peekable();
120                        while let Some(cur) = lines.next() {
121                            write!(f, "{cur}")?;
122                            if lines.peek().is_some() {
123                                write!(f, "{}", self.config.newline_style.as_str())?;
124                                write_indents(f, &self.config.indent, *num_indents)?;
125                                write!(f, "{prefix}")?;
126                            }
127                        }
128                        Ok(())
129                    }
130                    PostToken::Directive {
131                        num_indents,
132                        directive,
133                    } => {
134                        let mut prefix = format!("{} ", DIRECTIVE_COMMENT_PREFIX);
135                        match &**directive {
136                            Directive::Except(exceptions) => {
137                                prefix.push_str("except");
138                                prefix.push_str(DIRECTIVE_DELIMITER);
139                                prefix.push(' ');
140                                let mut rules: Vec<String> =
141                                    exceptions.iter().cloned().map(|e| e.name).collect();
142                                rules.sort();
143                                write!(f, "{prefix}")?;
144                                if let Some(max) = self.config.max_line_length.get() {
145                                    let indent_width = self.config.indent.num() * num_indents;
146                                    let start_width = indent_width + prefix.len();
147                                    let mut remaining = max.saturating_sub(start_width);
148                                    let mut written_to_cur_line = 0usize;
149                                    for rule in rules {
150                                        let cur_len = rule.len();
151                                        if written_to_cur_line == 0 {
152                                            write!(f, "{rule}")?;
153                                            remaining = remaining.saturating_sub(cur_len);
154                                            written_to_cur_line += 1;
155                                        } else if remaining.saturating_sub(cur_len + 2) > 0 {
156                                            // NOTE: the `+ 2` accounts for
157                                            // the `", "` separator written
158                                            // before each subsequent rule.
159                                            write!(f, ", {rule}")?;
160                                            remaining = remaining.saturating_sub(cur_len + 2);
161                                            written_to_cur_line += 1;
162                                        } else {
163                                            // Current rule does not fit
164                                            write!(f, "{}", self.config.newline_style.as_str())?;
165                                            write_indents(f, &self.config.indent, *num_indents)?;
166                                            write!(f, "{prefix}{rule}")?;
167                                            written_to_cur_line = 1;
168                                            remaining = max.saturating_sub(start_width + cur_len);
169                                        }
170                                    }
171                                    Ok(())
172                                } else {
173                                    write!(f, "{rules}", rules = rules.join(", "))
174                                }
175                            }
176                        }
177                    }
178                }
179            }
180        }
181
182        Display {
183            token: self,
184            config,
185        }
186    }
187}
188
189impl PostToken {
190    /// Gets the width of the [`PostToken`].
191    ///
192    /// This is used to determine how much space the token takes up _within a
193    /// single line_ for the purposes of respecting the maximum line length.
194    /// As such, newlines are considered zero-width tokens. Similarly, doc
195    /// comments and directive comments are considered zero-width as they always
196    /// appear on their own lines.
197    fn width(&self, config: &crate::Config) -> usize {
198        match self {
199            Self::Space => SPACE.len(), // 1 character
200            Self::Newline => 0,
201            Self::Indent => config.indent.num(),
202            Self::TempIndent(value) => value.len(),
203            Self::Literal(value) => value.len(),
204            Self::Directive { .. } => 0,
205            Self::Documentation { .. } => 0,
206        }
207    }
208}
209
210impl TokenStream<PostToken> {
211    /// Gets the maximum width of the [`TokenStream`].
212    ///
213    /// This is suitable to call if the stream represents multiple lines.
214    fn max_width(&self, config: &Config) -> usize {
215        let mut max: usize = 0;
216        let mut cur_width: usize = 0;
217        for token in self.iter() {
218            cur_width += token.width(config);
219            if token == &PostToken::Newline {
220                max = max.max(cur_width);
221                cur_width = 0;
222            }
223        }
224        max.max(cur_width)
225    }
226
227    /// Gets the width of the last line of the [`TokenStream`].
228    fn last_line_width(&self, config: &Config) -> usize {
229        let mut width = 0;
230        for token in self.iter().rev() {
231            if token == &PostToken::Newline {
232                break;
233            }
234            width += token.width(config);
235        }
236        width
237    }
238}
239
240/// A line break.
241enum LineBreak {
242    /// A line break that can be inserted before a token.
243    Before,
244    /// A line break that can be inserted after a token.
245    After,
246}
247
248/// Returns whether a token can be line broken.
249fn can_be_line_broken(kind: SyntaxKind) -> Option<LineBreak> {
250    match kind {
251        SyntaxKind::CloseBrace
252        | SyntaxKind::CloseBracket
253        | SyntaxKind::CloseParen
254        | SyntaxKind::CloseHeredoc
255        | SyntaxKind::Assignment
256        | SyntaxKind::Plus
257        | SyntaxKind::Minus
258        | SyntaxKind::Asterisk
259        | SyntaxKind::Slash
260        | SyntaxKind::Percent
261        | SyntaxKind::Exponentiation
262        | SyntaxKind::Equal
263        | SyntaxKind::NotEqual
264        | SyntaxKind::Less
265        | SyntaxKind::LessEqual
266        | SyntaxKind::Greater
267        | SyntaxKind::GreaterEqual
268        | SyntaxKind::LogicalAnd
269        | SyntaxKind::LogicalOr
270        | SyntaxKind::AfterKeyword
271        | SyntaxKind::AsKeyword
272        | SyntaxKind::IfKeyword
273        | SyntaxKind::ElseKeyword
274        | SyntaxKind::ThenKeyword => Some(LineBreak::Before),
275        SyntaxKind::OpenBrace
276        | SyntaxKind::OpenBracket
277        | SyntaxKind::OpenParen
278        | SyntaxKind::OpenHeredoc
279        | SyntaxKind::Colon
280        | SyntaxKind::PlaceholderOpen
281        | SyntaxKind::Comma => Some(LineBreak::After),
282        _ => None,
283    }
284}
285
286/// Gets the corresponding [`SyntaxKind`] that should be line broken in tandem
287/// with the provided [`SyntaxKind`].
288fn tandem_line_break(kind: SyntaxKind) -> Option<SyntaxKind> {
289    match kind {
290        SyntaxKind::OpenBrace => Some(SyntaxKind::CloseBrace),
291        SyntaxKind::OpenBracket => Some(SyntaxKind::CloseBracket),
292        SyntaxKind::OpenParen => Some(SyntaxKind::CloseParen),
293        SyntaxKind::OpenHeredoc => Some(SyntaxKind::CloseHeredoc),
294        SyntaxKind::PlaceholderOpen => Some(SyntaxKind::CloseBrace),
295        _ => None,
296    }
297}
298
299/// Tokens that should have a single indent popped from the
300/// stream if they are being added at the start of a line.
301fn should_deindent(kind: SyntaxKind) -> bool {
302    matches!(
303        kind,
304        SyntaxKind::OpenBrace
305            | SyntaxKind::OpenBracket
306            | SyntaxKind::OpenParen
307            | SyntaxKind::OpenHeredoc
308            | SyntaxKind::CloseBrace
309            | SyntaxKind::CloseBracket
310            | SyntaxKind::CloseParen
311            | SyntaxKind::CloseHeredoc
312    )
313}
314
315/// Tracks a tandem break.
316struct TandemBreak {
317    /// The [`SyntaxKind`] which opened this tandem break.
318    pub open: SyntaxKind,
319    /// The [`SyntaxKind`] which will close this tandem break.
320    pub close: SyntaxKind,
321    /// Token depth since opening the break.
322    ///
323    /// The close break is only added when `depth == 0`.
324    /// This is incremented by one for every token matching `open` after the
325    /// break is initiated. It is decremented by one for every token
326    /// matching `close` after the break is initiated.
327    pub depth: usize,
328}
329
330/// Current position in a line.
331#[derive(Default, Eq, PartialEq)]
332enum LinePosition {
333    /// The start of a line.
334    #[default]
335    StartOfLine,
336
337    /// The middle of a line.
338    MiddleOfLine,
339}
340
341/// A postprocessor of [tokens](PreToken).
342#[derive(Default)]
343pub struct Postprocessor {
344    /// The current position in the line.
345    position: LinePosition,
346
347    /// The current indentation level.
348    indent_level: usize,
349
350    /// Whether the current line has been interrupted by trivia.
351    interrupted: bool,
352
353    /// The current trivial blank line spacing policy.
354    line_spacing_policy: TriviaBlankLineSpacingPolicy,
355
356    /// Temporary indentation to add.
357    temp_indent: Option<Rc<String>>,
358}
359
360impl Postprocessor {
361    /// Runs the postprocessor.
362    pub fn run(&mut self, input: TokenStream<PreToken>, config: &Config) -> TokenStream<PostToken> {
363        let mut output = TokenStream::<PostToken>::default();
364        let mut buffer = TokenStream::<PreToken>::default();
365
366        for token in input {
367            match token {
368                PreToken::LineEnd => {
369                    self.flush(&buffer, &mut output, config);
370                    self.trim_whitespace(&mut output);
371                    output.push(PostToken::Newline);
372
373                    buffer.clear();
374                    self.interrupted = false;
375                    self.position = LinePosition::StartOfLine;
376                }
377                _ => {
378                    buffer.push(token);
379                }
380            }
381        }
382
383        output
384    }
385
386    /// Takes a step of a [`PreToken`] stream and processes the appropriate
387    /// [`PostToken`]s.
388    fn step(
389        &mut self,
390        token: PreToken,
391        next: Option<&PreToken>,
392        stream: &mut TokenStream<PostToken>,
393    ) {
394        if stream.is_empty() {
395            self.interrupted = false;
396            self.position = LinePosition::StartOfLine;
397            self.indent(stream);
398        }
399        match token {
400            PreToken::BlankLine => {
401                self.blank_line(stream);
402            }
403            PreToken::LineEnd => {
404                self.interrupted = false;
405                self.end_line(stream);
406            }
407            PreToken::WordEnd => {
408                stream.trim_end(&PostToken::Space);
409
410                if self.position == LinePosition::MiddleOfLine {
411                    stream.push(PostToken::Space);
412                } else {
413                    // We're at the start of a line, so we don't need to add a
414                    // space.
415                }
416            }
417            PreToken::IndentStart => {
418                self.indent_level += 1;
419                self.end_line(stream);
420            }
421            PreToken::IndentEnd => {
422                self.indent_level = self.indent_level.saturating_sub(1);
423                self.end_line(stream);
424            }
425            PreToken::LineSpacingPolicy(policy) => {
426                self.line_spacing_policy = policy;
427            }
428            PreToken::Literal(value, kind) => {
429                assert!(!kind.is_trivia());
430
431                // This is special handling for inserting the empty string.
432                // We remove any indentation or spaces from the end of the
433                // stream before adding the empty string as a literal.
434                if value.is_empty() {
435                    self.trim_last_line(stream);
436                }
437
438                if self.interrupted
439                    && should_deindent(kind)
440                    && matches!(
441                        stream.0.last(),
442                        Some(&PostToken::Indent) | Some(&PostToken::TempIndent(_))
443                    )
444                {
445                    let popped = stream.0.pop().unwrap();
446                    // We don't actually want to pop the TempIndent token,
447                    // but rather a regular Indent token before the temp indent.
448                    if matches!(popped, PostToken::TempIndent(_)) {
449                        stream.0.pop_if(|t| matches!(t, PostToken::Indent));
450                        // Restore the popped TempIndent
451                        stream.0.push(popped);
452                    }
453                }
454
455                stream.push(PostToken::Literal(value));
456                self.position = LinePosition::MiddleOfLine;
457            }
458            PreToken::Trivia(trivia) => match trivia {
459                Trivia::BlankLine => match self.line_spacing_policy {
460                    TriviaBlankLineSpacingPolicy::Always => {
461                        self.blank_line(stream);
462                    }
463                    TriviaBlankLineSpacingPolicy::RemoveTrailingBlanks => {
464                        if matches!(next, Some(&PreToken::Trivia(Trivia::Comment(_)))) {
465                            self.blank_line(stream);
466                        }
467                    }
468                },
469                Trivia::Comment(comment) => {
470                    match comment {
471                        Comment::Preceding(value) => {
472                            if self.position == LinePosition::MiddleOfLine {
473                                self.interrupted = true;
474                                self.end_line(stream);
475                            }
476                            stream.push(PostToken::Literal(value));
477                        }
478                        Comment::Inline(value) => {
479                            assert!(self.position == LinePosition::MiddleOfLine);
480                            if let Some(next) = next
481                                && next != &PreToken::LineEnd
482                            {
483                                self.interrupted = true;
484                            }
485                            self.trim_last_line(stream);
486                            for token in INLINE_COMMENT_PRECEDING_TOKENS.iter() {
487                                stream.push(token.clone());
488                            }
489                            stream.push(PostToken::Literal(value));
490                        }
491                        Comment::Documentation(contents) => {
492                            if self.position == LinePosition::MiddleOfLine {
493                                self.interrupted = true;
494                                self.end_line(stream);
495                            }
496                            stream.push(PostToken::Documentation {
497                                num_indents: self.indent_level,
498                                contents,
499                            });
500                        }
501                        Comment::Directive(directive) => {
502                            if self.position == LinePosition::MiddleOfLine {
503                                self.interrupted = true;
504                                self.end_line(stream);
505                            }
506                            stream.push(PostToken::Directive {
507                                num_indents: self.indent_level,
508                                directive,
509                            });
510                        }
511                    }
512                    self.position = LinePosition::MiddleOfLine;
513                    self.end_line(stream);
514                }
515            },
516            PreToken::TempIndentStart(bash_indent) => {
517                self.temp_indent = Some(bash_indent);
518            }
519            PreToken::TempIndentEnd => {
520                self.temp_indent = None;
521            }
522        }
523    }
524
525    /// Flushes the `in_stream` buffer to the `out_stream`.
526    fn flush(
527        &mut self,
528        in_stream: &TokenStream<PreToken>,
529        out_stream: &mut TokenStream<PostToken>,
530        config: &Config,
531    ) {
532        assert!(!self.interrupted);
533        assert!(self.position == LinePosition::StartOfLine);
534        let mut post_buffer = TokenStream::<PostToken>::default();
535        let mut pre_buffer = in_stream.iter().peekable();
536        let starting_indent = self.indent_level;
537        let starting_temp_indent = self.temp_indent.clone();
538        while let Some(token) = pre_buffer.next() {
539            let next = pre_buffer.peek().copied();
540            self.step(token.clone(), next, &mut post_buffer);
541        }
542
543        // If all lines are short enough, we can just add the post_buffer to the
544        // out_stream and be done.
545        if config.max_line_length.get().is_none()
546            || post_buffer.max_width(config) <= config.max_line_length.get().unwrap()
547        {
548            out_stream.extend(post_buffer);
549            return;
550        }
551
552        // At least one line in the post_buffer is too long.
553        // We iterate through the in_stream to find potential line breaks,
554        // and then we iterate through the in_stream again to actually insert
555        // them in the proper places.
556
557        let max_length = config.max_line_length.get().unwrap();
558
559        let mut potential_line_breaks: HashMap<usize, SyntaxKind> = HashMap::new();
560        for (i, token) in in_stream.iter().enumerate() {
561            if let PreToken::Literal(_, kind) = token {
562                match can_be_line_broken(*kind) {
563                    Some(LineBreak::Before) => {
564                        potential_line_breaks.insert(i, *kind);
565                    }
566                    Some(LineBreak::After) => {
567                        potential_line_breaks.insert(i + 1, *kind);
568                    }
569                    None => {}
570                }
571            }
572        }
573
574        if potential_line_breaks.is_empty() {
575            // There are no potential line breaks, so we can't do anything.
576            out_stream.extend(post_buffer);
577            return;
578        }
579
580        // Set up the buffers for the second pass.
581        post_buffer.clear();
582        let mut pre_buffer = in_stream.iter().enumerate().peekable();
583
584        // Reset self.
585        self.interrupted = false;
586        self.position = LinePosition::StartOfLine;
587        self.temp_indent = starting_temp_indent;
588        self.indent_level = starting_indent;
589
590        let mut break_stack: Vec<TandemBreak> = Vec::new();
591
592        while let Some((i, token)) = pre_buffer.next() {
593            let mut cache = None;
594            if let Some(break_kind) = potential_line_breaks.get(&i) {
595                // Check if we need a break to match a prior tandem break
596                if let Some(top_of_stack) = break_stack.last_mut() {
597                    if *break_kind == top_of_stack.close {
598                        if top_of_stack.depth > 0 {
599                            top_of_stack.depth -= 1;
600                        } else {
601                            break_stack.pop();
602                            self.indent_level -= 1;
603                            self.end_line(&mut post_buffer);
604                        }
605                    } else if *break_kind == top_of_stack.open {
606                        top_of_stack.depth += 1;
607                    }
608                }
609                // Cache the current state so we can revert to it if
610                // necessary.
611                cache = Some(post_buffer.clone());
612            }
613
614            self.step(
615                token.clone(),
616                pre_buffer.peek().map(|(_, v)| &**v),
617                &mut post_buffer,
618            );
619
620            if let Some(cache) = cache
621                && post_buffer.last_line_width(config) > max_length
622            {
623                // The line is too long after the next step. Revert to the
624                // cached state and insert a line break.
625                post_buffer = cache;
626                self.interrupted = true;
627                self.end_line(&mut post_buffer);
628                self.step(
629                    token.clone(),
630                    pre_buffer.peek().map(|(_, v)| &**v),
631                    &mut post_buffer,
632                );
633
634                // Check if this introduces a tandem break
635                // SAFETY: if cache is Some(_) this step must have a potential line break
636                let break_kind = potential_line_breaks.get(&i).unwrap();
637                if let Some(also_break_on) = tandem_line_break(*break_kind) {
638                    let tandem_break = TandemBreak {
639                        open: *break_kind,
640                        close: also_break_on,
641                        depth: 0,
642                    };
643                    break_stack.push(tandem_break);
644                    self.indent_level += 1;
645                }
646            }
647        }
648
649        // reduce indent for breaks never added
650        for _ in break_stack {
651            self.indent_level = self.indent_level.saturating_sub(1);
652        }
653        out_stream.extend(post_buffer);
654    }
655
656    /// Trims any and all whitespace from the end of the stream.
657    fn trim_whitespace(&self, stream: &mut TokenStream<PostToken>) {
658        stream.trim_while(|token| {
659            matches!(
660                token,
661                PostToken::Space
662                    | PostToken::Newline
663                    | PostToken::Indent
664                    | PostToken::TempIndent(_)
665            )
666        });
667    }
668
669    /// Trims spaces and indents (and not newlines) from the end of the stream.
670    fn trim_last_line(&self, stream: &mut TokenStream<PostToken>) {
671        stream.trim_while(|token| {
672            matches!(
673                token,
674                PostToken::Space | PostToken::Indent | PostToken::TempIndent(_)
675            )
676        });
677    }
678
679    /// Ends the current line without resetting the interrupted flag.
680    ///
681    /// Removes any trailing spaces or indents and adds a newline only if state
682    /// is not [`LinePosition::StartOfLine`]. State is then set to
683    /// [`LinePosition::StartOfLine`]. Finally, indentation is added. Safe to
684    /// call multiple times in a row.
685    fn end_line(&mut self, stream: &mut TokenStream<PostToken>) {
686        self.trim_last_line(stream);
687        if self.position != LinePosition::StartOfLine {
688            stream.push(PostToken::Newline);
689        }
690        self.position = LinePosition::StartOfLine;
691        self.indent(stream);
692    }
693
694    /// Pushes the current indentation level to the stream.
695    ///
696    /// This should only be called when the state is
697    /// [`LinePosition::StartOfLine`]. This does not change the state
698    /// and is safe to call multiple times in a row.
699    fn indent(&self, stream: &mut TokenStream<PostToken>) {
700        assert!(self.position == LinePosition::StartOfLine);
701
702        self.trim_last_line(stream);
703
704        let level = if self.interrupted {
705            self.indent_level + 1
706        } else {
707            self.indent_level
708        };
709
710        for _ in 0..level {
711            stream.push(PostToken::Indent);
712        }
713
714        if let Some(ref temp_indent) = self.temp_indent {
715            stream.push(PostToken::TempIndent(temp_indent.clone()));
716        }
717    }
718
719    /// Creates a blank line and then indents.
720    fn blank_line(&mut self, stream: &mut TokenStream<PostToken>) {
721        self.trim_whitespace(stream);
722        if !stream.is_empty() {
723            stream.push(PostToken::Newline);
724        }
725        stream.push(PostToken::Newline);
726        self.position = LinePosition::StartOfLine;
727        self.indent(stream);
728    }
729}