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