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