Skip to main content

dprint_plugin_typescript/generation/
context.rs

1use deno_ast::swc::common::comments::Comment;
2use deno_ast::swc::parser::token::TokenAndSpan;
3use deno_ast::view::*;
4use deno_ast::MediaType;
5use deno_ast::SourcePos;
6use deno_ast::SourceRange;
7use deno_ast::SourceRanged;
8use deno_ast::SourceRangedForSpanned;
9use dprint_core::formatting::ConditionReference;
10use dprint_core::formatting::IndentLevel;
11use dprint_core::formatting::IsStartOfLine;
12use dprint_core::formatting::LineNumber;
13use dprint_core::formatting::LineStartIndentLevel;
14use rustc_hash::FxHashMap;
15use rustc_hash::FxHashSet;
16
17use super::*;
18use crate::configuration::*;
19use crate::utils::Stack;
20
21/// A callback that will be called when encountering tagged templates.
22///
23/// It is up to the caller to decide if a certain tagged template should be formatted
24/// by the external formatter.
25///
26/// Examples:
27/// ```ignore
28/// const styles = css`color: red;`;
29///
30/// const markup = html`<html>
31///   <body>
32///     <h1>Hello!<h1>
33///   </body>
34/// </html>`;
35///
36/// const query = sql`
37/// SELECT
38///   *
39/// FROM
40///   users
41/// WHERE
42///   active IS TRUE;
43/// ```
44///
45/// External formatter should return `None` if it doesn't understand given language, in such
46/// cases the templates will be left as they are.
47///
48/// Only templates with no interpolation are supported.
49pub type ExternalFormatter = dyn Fn(&str, String, &Configuration) -> anyhow::Result<Option<String>>;
50
51pub(crate) struct GenerateDiagnostic {
52  pub message: String,
53}
54
55pub struct Context<'a> {
56  pub media_type: MediaType,
57  pub program: Program<'a>,
58  pub config: &'a Configuration,
59  pub comments: CommentTracker<'a>,
60  pub external_formatter: Option<&'a ExternalFormatter>,
61  pub token_finder: TokenFinder<'a>,
62  pub current_node: Node<'a>,
63  pub parent_stack: Stack<Node<'a>>,
64  /// Stores whether the parent requires all properties have consistent quoting.
65  consistent_quote_props_stack: Stack<bool>,
66  handled_comments: FxHashSet<SourcePos>,
67  stored_ln_ranges: FxHashMap<(SourcePos, SourcePos), (LineNumber, LineNumber)>,
68  stored_lsil: FxHashMap<(SourcePos, SourcePos), LineStartIndentLevel>,
69  stored_ln: FxHashMap<(SourcePos, SourcePos), LineNumber>,
70  stored_il: FxHashMap<(SourcePos, SourcePos), IndentLevel>,
71  pub skip_iife_body_indent: bool,
72  pub end_statement_or_member_lns: Stack<LineNumber>,
73  before_comments_start_info_stack: Stack<(SourceRange, LineNumber, IsStartOfLine)>,
74  if_stmt_last_brace_condition_ref: Option<ConditionReference>,
75  expr_stmt_single_line_parent_brace_ref: Option<ConditionReference>,
76  /// Used for ensuring nodes are parsed in order.
77  #[cfg(debug_assertions)]
78  pub last_generated_node_pos: SourcePos,
79  pub diagnostics: Vec<GenerateDiagnostic>,
80}
81
82impl<'a> Context<'a> {
83  pub fn new(
84    media_type: MediaType,
85    tokens: &'a [TokenAndSpan],
86    current_node: Node<'a>,
87    program: Program<'a>,
88    config: &'a Configuration,
89    external_formatter: Option<&'a ExternalFormatter>,
90  ) -> Context<'a> {
91    Context {
92      media_type,
93      program,
94      config,
95      comments: CommentTracker::new(program, tokens),
96      external_formatter,
97      token_finder: TokenFinder::new(program),
98      current_node,
99      parent_stack: Default::default(),
100      consistent_quote_props_stack: Default::default(),
101      handled_comments: FxHashSet::default(),
102      stored_ln_ranges: FxHashMap::default(),
103      stored_lsil: FxHashMap::default(),
104      stored_ln: FxHashMap::default(),
105      stored_il: FxHashMap::default(),
106      skip_iife_body_indent: false,
107      end_statement_or_member_lns: Default::default(),
108      before_comments_start_info_stack: Default::default(),
109      if_stmt_last_brace_condition_ref: None,
110      expr_stmt_single_line_parent_brace_ref: None,
111      #[cfg(debug_assertions)]
112      last_generated_node_pos: deno_ast::SourceTextInfoProvider::text_info(&program).range().start.into(),
113      diagnostics: Vec::new(),
114    }
115  }
116
117  pub fn is_jsx(&self) -> bool {
118    matches!(self.media_type, MediaType::Tsx | MediaType::Jsx | MediaType::JavaScript)
119  }
120
121  pub fn parent(&self) -> Node<'a> {
122    *self.parent_stack.peek().unwrap()
123  }
124
125  pub fn has_handled_comment(&self, comment: &Comment) -> bool {
126    self.handled_comments.contains(&comment.start())
127  }
128
129  pub fn mark_comment_handled(&mut self, comment: &Comment) {
130    self.handled_comments.insert(comment.start());
131  }
132
133  pub fn store_info_range_for_node(&mut self, node: &impl SourceRanged, lns: (LineNumber, LineNumber)) {
134    self.stored_ln_ranges.insert((node.start(), node.end()), lns);
135  }
136
137  pub fn get_ln_range_for_node(&self, node: &impl SourceRanged) -> Option<(LineNumber, LineNumber)> {
138    self.stored_ln_ranges.get(&(node.start(), node.end())).map(|x| x.to_owned())
139  }
140
141  pub fn store_lsil_for_node(&mut self, node: &impl SourceRanged, lsil: LineStartIndentLevel) {
142    self.stored_lsil.insert((node.start(), node.end()), lsil);
143  }
144
145  pub fn get_lsil_for_node(&self, node: &impl SourceRanged) -> Option<LineStartIndentLevel> {
146    self.stored_lsil.get(&(node.start(), node.end())).map(|x| x.to_owned())
147  }
148
149  pub fn store_ln_for_node(&mut self, node: &impl SourceRanged, ln: LineNumber) {
150    self.stored_ln.insert((node.start(), node.end()), ln);
151  }
152
153  pub fn get_ln_for_node(&self, node: &impl SourceRanged) -> Option<LineNumber> {
154    self.stored_ln.get(&(node.start(), node.end())).map(|x| x.to_owned())
155  }
156
157  pub fn store_il_for_node(&mut self, node: &impl SourceRanged, il: IndentLevel) {
158    self.stored_il.insert((node.start(), node.end()), il);
159  }
160
161  pub fn get_il_for_node(&self, node: &impl SourceRanged) -> Option<IndentLevel> {
162    self.stored_il.get(&(node.start(), node.end())).map(|x| x.to_owned())
163  }
164
165  pub fn store_if_stmt_last_brace_condition_ref(&mut self, condition_reference: ConditionReference) {
166    self.if_stmt_last_brace_condition_ref = Some(condition_reference);
167  }
168
169  pub fn take_if_stmt_last_brace_condition_ref(&mut self) -> Option<ConditionReference> {
170    self.if_stmt_last_brace_condition_ref.take()
171  }
172
173  pub fn store_expr_stmt_single_line_parent_brace_ref(&mut self, condition_reference: ConditionReference) {
174    self.expr_stmt_single_line_parent_brace_ref = Some(condition_reference);
175  }
176
177  pub fn take_expr_stmt_single_line_parent_brace_ref(&mut self) -> Option<ConditionReference> {
178    self.expr_stmt_single_line_parent_brace_ref.take()
179  }
180
181  pub fn get_or_create_current_before_comments_start_info(&mut self) -> (LineNumber, IsStartOfLine) {
182    let current_range = self.current_node.range();
183    if let Some((range, ln, isol)) = self.before_comments_start_info_stack.peek() {
184      if *range == current_range {
185        return (*ln, *isol);
186      }
187    }
188
189    let new_ln = LineNumber::new("beforeComments");
190    let new_isol = IsStartOfLine::new("beforeComments");
191    self.before_comments_start_info_stack.push((current_range, new_ln, new_isol));
192    (new_ln, new_isol)
193  }
194
195  pub fn take_current_before_comments_start_info(&mut self) -> Option<(LineNumber, IsStartOfLine)> {
196    let mut had_range = false;
197    if let Some((range, _, _)) = self.before_comments_start_info_stack.peek() {
198      if *range == self.current_node.range() {
199        had_range = true;
200      }
201    }
202
203    if had_range {
204      let (_, ln, isol) = self.before_comments_start_info_stack.pop();
205      Some((ln, isol))
206    } else {
207      None
208    }
209  }
210
211  pub fn use_consistent_quote_props(&self) -> Option<bool> {
212    self.consistent_quote_props_stack.peek().copied()
213  }
214
215  pub fn with_maybe_consistent_props<TState, TReturn>(
216    &mut self,
217    state: TState,
218    use_consistent_quotes: impl FnOnce(&TState) -> bool,
219    action: impl FnOnce(&mut Self, TState) -> TReturn,
220  ) -> TReturn {
221    if self.config.quote_props == QuoteProps::Consistent {
222      self.consistent_quote_props_stack.push((use_consistent_quotes)(&state));
223      let result = action(self, state);
224      self.consistent_quote_props_stack.pop();
225      result
226    } else {
227      action(self, state)
228    }
229  }
230
231  // do any assertions for how the state of this context should be at the end of the file
232  #[cfg(debug_assertions)]
233  pub fn assert_end_of_file_state(&self) {
234    if self.before_comments_start_info_stack.iter().next().is_some() {
235      panic!("Debug panic! There were infos in the before comments start info stack.");
236    }
237  }
238
239  #[cfg(debug_assertions)]
240  pub fn assert_text(&self, range: SourceRange, expected_text: &str) {
241    let actual_text = range.text_fast(self.program);
242    if actual_text != expected_text {
243      panic!("Debug Panic Expected text `{expected_text}`, but found `{actual_text}`")
244    }
245  }
246}