microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.3.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

compiler/qsc_formatter/src/formatter.rs

485lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use qsc_data_structures::span::Span;
5use qsc_frontend::{
6 keyword::Keyword,
7 lex::{
8 concrete::{self, ConcreteToken, ConcreteTokenKind},
9 cooked::{StringToken, TokenKind},
10 Delim, InterpolatedEnding, InterpolatedStart,
11 },
12};
13
14#[cfg(test)]
15mod tests;
16
17#[derive(Debug)]
18pub struct TextEdit {
19 pub new_text: String,
20 pub span: Span,
21}
22
23impl TextEdit {
24 fn new(new_text: &str, lo: u32, hi: u32) -> Self {
25 Self {
26 new_text: new_text.to_string(),
27 span: Span { lo, hi },
28 }
29 }
30}
31
32fn make_indent_string(level: usize) -> String {
33 " ".repeat(level)
34}
35
36/// Applies formatting rules to the give code str and returns
37/// the formatted string.
38pub fn format_str(code: &str) -> String {
39 let mut edits = calculate_format_edits(code);
40 edits.sort_by_key(|edit| edit.span.hi); // sort edits by their span's hi value from lowest to highest
41 edits.reverse(); // sort from highest to lowest so that that as edits are applied they don't invalidate later applications of edits
42 let mut new_code = String::from(code);
43
44 for edit in edits {
45 let range = (edit.span.lo as usize)..(edit.span.hi as usize);
46 new_code.replace_range(range, &edit.new_text);
47 }
48
49 new_code
50}
51
52/// Applies formatting rules to the given code str, generating edits where
53/// the source code needs to be changed to comply with the format rules.
54pub fn calculate_format_edits(code: &str) -> Vec<TextEdit> {
55 let tokens = concrete::ConcreteTokenIterator::new(code);
56 let mut edits = vec![];
57
58 let mut indent_level: usize = 0;
59
60 // The sliding window used is over three adjacent tokens
61 #[allow(unused_assignments)]
62 let mut one = None;
63 let mut two = None;
64 let mut three = None;
65
66 for token in tokens {
67 // Advance the token window
68 one = two;
69 two = three;
70 three = Some(token);
71
72 let mut edits_for_triple = match (&one, &two, &three) {
73 (Some(one), Some(two), Some(three)) => {
74 match one.kind {
75 ConcreteTokenKind::Syntax(TokenKind::Open(Delim::Brace)) => indent_level += 1,
76 ConcreteTokenKind::Syntax(TokenKind::Close(Delim::Brace)) => {
77 indent_level = indent_level.saturating_sub(1)
78 }
79 ConcreteTokenKind::WhiteSpace => continue,
80 _ => {}
81 }
82
83 if matches!(one.kind, ConcreteTokenKind::WhiteSpace) {
84 // first token is whitespace, continue scanning
85 continue;
86 } else if matches!(two.kind, ConcreteTokenKind::WhiteSpace) {
87 // whitespace in the middle
88 apply_rules(
89 one,
90 get_token_contents(code, two),
91 three,
92 code,
93 indent_level,
94 )
95 } else {
96 // one, two are adjacent tokens with no whitespace in the middle
97 apply_rules(one, "", two, code, indent_level)
98 }
99 }
100 (None, None, Some(three)) => {
101 // Remove any whitespace at the start of a file
102 if matches!(three.kind, ConcreteTokenKind::WhiteSpace) {
103 vec![TextEdit::new("", three.span.lo, three.span.hi)]
104 } else {
105 vec![]
106 }
107 }
108 _ => {
109 // not enough tokens to apply a rule
110 continue;
111 }
112 };
113
114 edits.append(&mut edits_for_triple);
115 }
116
117 edits
118}
119
120fn apply_rules(
121 left: &ConcreteToken,
122 whitespace: &str,
123 right: &ConcreteToken,
124 code: &str,
125 indent_level: usize,
126) -> Vec<TextEdit> {
127 let mut edits = vec![];
128 // when we get here, neither left nor right should be whitespace
129
130 // if the right is a close brace, the indent level should be one less
131 let indent_level = if let ConcreteTokenKind::Syntax(TokenKind::Close(Delim::Brace)) = right.kind
132 {
133 indent_level.saturating_sub(1)
134 } else {
135 indent_level
136 };
137
138 let new_line_in_spaces = whitespace.contains('\n');
139
140 use qsc_frontend::keyword::Keyword;
141 use qsc_frontend::lex::cooked::ClosedBinOp;
142 use ConcreteTokenKind::*;
143 use TokenKind::*;
144 match (&left.kind, &right.kind) {
145 (Comment | Syntax(DocComment), _) => {
146 // remove whitespace at the ends of comments
147 effect_trim_comment(left, &mut edits, code);
148 effect_correct_indentation(left, whitespace, right, &mut edits, indent_level);
149 }
150 (_, Comment) => {
151 if new_line_in_spaces {
152 effect_correct_indentation(left, whitespace, right, &mut edits, indent_level);
153 }
154 }
155 (Syntax(cooked_left), Syntax(cooked_right)) => match (cooked_left, cooked_right) {
156 (ClosedBinOp(ClosedBinOp::Minus), _) | (_, ClosedBinOp(ClosedBinOp::Minus)) => {
157 // This case is used to ignore the spacing around a `-`.
158 // This is done because we currently don't have the architecture
159 // to be able to differentiate between the unary `-` and the binary `-`
160 // which would have different spacing rules.
161 }
162 (Gt, _) | (_, Gt) | (Lt, _) | (_, Lt) => {
163 // This case is used to ignore the spacing around a `<` and `>`.
164 // This is done because we currently don't have the architecture
165 // to be able to differentiate between the comparison operators
166 // and the type-parameter delimiters which would have different
167 // spacing rules.
168 }
169 (Semi, _) => {
170 effect_correct_indentation(left, whitespace, right, &mut edits, indent_level);
171 }
172 (_, Semi) => {
173 effect_no_space(left, whitespace, right, &mut edits);
174 }
175 (Open(l), Close(r)) if l == r => {
176 // close empty delimiter blocks, i.e. (), [], {}
177 effect_no_space(left, whitespace, right, &mut edits);
178 }
179 (At, Ident) => {
180 effect_no_space(left, whitespace, right, &mut edits);
181 }
182 (Keyword(Keyword::Internal), _) => {
183 effect_single_space(left, whitespace, right, &mut edits);
184 }
185 (Keyword(Keyword::Adjoint), Keyword(Keyword::Controlled))
186 | (Keyword(Keyword::Controlled), Keyword(Keyword::Adjoint)) => {
187 effect_single_space(left, whitespace, right, &mut edits);
188 }
189 (Open(Delim::Brace), _)
190 | (_, Close(Delim::Brace))
191 | (_, Keyword(Keyword::Internal))
192 | (_, Keyword(Keyword::Operation))
193 | (_, Keyword(Keyword::Function))
194 | (_, Keyword(Keyword::Newtype))
195 | (_, Keyword(Keyword::Namespace))
196 | (_, Keyword(Keyword::Open))
197 | (_, Keyword(Keyword::Body))
198 | (_, Keyword(Keyword::Adjoint))
199 | (_, Keyword(Keyword::Controlled))
200 | (_, Keyword(Keyword::Let))
201 | (_, Keyword(Keyword::Mutable))
202 | (_, Keyword(Keyword::Set))
203 | (_, Keyword(Keyword::Use))
204 | (_, Keyword(Keyword::Borrow))
205 | (_, Keyword(Keyword::Fixup))
206 | (_, At) => {
207 effect_correct_indentation(left, whitespace, right, &mut edits, indent_level);
208 }
209 (_, TokenKind::Keyword(Keyword::Until))
210 | (_, TokenKind::Keyword(Keyword::In))
211 | (_, TokenKind::Keyword(Keyword::As))
212 | (_, TokenKind::Keyword(Keyword::Elif))
213 | (_, TokenKind::Keyword(Keyword::Else))
214 | (_, TokenKind::Keyword(Keyword::Apply)) => {
215 effect_single_space(left, whitespace, right, &mut edits);
216 }
217 (_, TokenKind::Keyword(Keyword::Auto))
218 | (_, TokenKind::Keyword(Keyword::Distribute))
219 | (_, TokenKind::Keyword(Keyword::Intrinsic))
220 | (_, TokenKind::Keyword(Keyword::Invert))
221 | (_, TokenKind::Keyword(Keyword::Slf)) => {
222 effect_single_space(left, whitespace, right, &mut edits);
223 }
224 (_, _) if new_line_in_spaces => {
225 effect_trim_whitespace(left, whitespace, right, &mut edits);
226 // Ignore the rest of the cases if the user has a newline in the whitespace.
227 // This is done because we don't currently have logic for determining when
228 // lines are too long and require newlines, and we don't have logic
229 // for determining what the correct indentation should be in these cases,
230 // so we put this do-nothing case in to leave user code unchanged.
231 }
232 (String(StringToken::Interpolated(_, InterpolatedEnding::LBrace)), _)
233 | (_, String(StringToken::Interpolated(InterpolatedStart::RBrace, _))) => {
234 effect_no_space(left, whitespace, right, &mut edits);
235 }
236 (Open(Delim::Bracket | Delim::Paren), _)
237 | (_, Close(Delim::Bracket | Delim::Paren)) => {
238 effect_no_space(left, whitespace, right, &mut edits);
239 }
240 (_, Open(Delim::Bracket | Delim::Paren)) => {
241 if is_value_token_left(cooked_left) || is_prefix(cooked_left) {
242 // i.e. foo() or { foo }[3]
243 effect_no_space(left, whitespace, right, &mut edits);
244 } else {
245 // i.e. let x = (1, 2, 3);
246 effect_single_space(left, whitespace, right, &mut edits);
247 }
248 }
249 (_, TokenKind::DotDotDot) => {
250 if is_value_token_left(cooked_left) {
251 effect_no_space(left, whitespace, right, &mut edits);
252 } else {
253 effect_single_space(left, whitespace, right, &mut edits);
254 }
255 }
256 (TokenKind::DotDotDot, TokenKind::Open(Delim::Brace)) => {
257 // Special case: `... {}`
258 effect_single_space(left, whitespace, right, &mut edits);
259 }
260 (_, TokenKind::Keyword(Keyword::Is))
261 | (_, TokenKind::Keyword(Keyword::For))
262 | (_, TokenKind::Keyword(Keyword::While))
263 | (_, TokenKind::Keyword(Keyword::Repeat))
264 | (_, TokenKind::Keyword(Keyword::If))
265 | (_, TokenKind::Keyword(Keyword::Within))
266 | (_, TokenKind::Keyword(Keyword::Return))
267 | (_, TokenKind::Keyword(Keyword::Fail)) => {
268 effect_single_space(left, whitespace, right, &mut edits);
269 }
270 (_, _) if is_value_token_right(cooked_right) => {
271 if is_prefix(cooked_left) {
272 effect_no_space(left, whitespace, right, &mut edits);
273 } else {
274 effect_single_space(left, whitespace, right, &mut edits);
275 }
276 }
277 (_, _) if is_suffix(cooked_right) => {
278 effect_no_space(left, whitespace, right, &mut edits);
279 }
280 (_, _) if is_prefix_with_space(cooked_right) => {
281 if is_prefix(cooked_left) {
282 effect_no_space(left, whitespace, right, &mut edits);
283 } else {
284 effect_single_space(left, whitespace, right, &mut edits);
285 }
286 }
287 (_, _) if is_prefix_without_space(cooked_right) => {
288 effect_no_space(left, whitespace, right, &mut edits);
289 }
290 (_, _) if is_bin_op(cooked_right) => {
291 effect_single_space(left, whitespace, right, &mut edits);
292 }
293 _ => {}
294 },
295 _ => {}
296 }
297 edits
298}
299
300fn is_bin_op(cooked: &TokenKind) -> bool {
301 matches!(
302 cooked,
303 TokenKind::Bar
304 | TokenKind::BinOpEq(_)
305 | TokenKind::ClosedBinOp(_)
306 | TokenKind::Colon
307 | TokenKind::Eq
308 | TokenKind::EqEq
309 | TokenKind::FatArrow
310 | TokenKind::Gt
311 | TokenKind::Gte
312 | TokenKind::LArrow
313 | TokenKind::Lt
314 | TokenKind::Lte
315 | TokenKind::Ne
316 | TokenKind::Question
317 | TokenKind::RArrow
318 | TokenKind::WSlash
319 | TokenKind::WSlashEq
320 | TokenKind::Keyword(Keyword::And)
321 | TokenKind::Keyword(Keyword::Or)
322 // Technically the rest are not binary ops, but has the same spacing as one
323 | TokenKind::Keyword(Keyword::Not)
324 | TokenKind::Keyword(Keyword::AdjointUpper)
325 | TokenKind::Keyword(Keyword::ControlledUpper)
326 )
327}
328
329fn is_prefix_with_space(cooked: &TokenKind) -> bool {
330 matches!(cooked, TokenKind::TildeTildeTilde)
331}
332
333fn is_prefix_without_space(cooked: &TokenKind) -> bool {
334 matches!(
335 cooked,
336 TokenKind::ColonColon | TokenKind::Dot | TokenKind::DotDot
337 )
338}
339
340fn is_prefix(cooked: &TokenKind) -> bool {
341 is_prefix_with_space(cooked)
342 || is_prefix_without_space(cooked)
343 || matches!(cooked, TokenKind::DotDotDot)
344}
345
346fn is_suffix(cooked: &TokenKind) -> bool {
347 matches!(cooked, TokenKind::Bang | TokenKind::Comma)
348}
349
350fn is_keyword_value(keyword: &Keyword) -> bool {
351 use Keyword::*;
352 matches!(
353 keyword,
354 True | False | Zero | One | PauliI | PauliX | PauliY | PauliZ | Underscore
355 // Adj and Ctl are not really values, but have the same spacing as values
356 | Adj | Ctl
357 )
358}
359
360/// Note that this does not include interpolated string literals
361fn is_value_lit(cooked: &TokenKind) -> bool {
362 matches!(
363 cooked,
364 TokenKind::BigInt(_)
365 | TokenKind::Float
366 | TokenKind::Ident
367 | TokenKind::AposIdent
368 | TokenKind::Int(_)
369 | TokenKind::String(StringToken::Normal)
370 )
371}
372
373fn is_value_token_left(cooked: &TokenKind) -> bool {
374 match cooked {
375 _ if is_value_lit(cooked) => true,
376 TokenKind::String(StringToken::Interpolated(_, InterpolatedEnding::Quote)) => true,
377 TokenKind::Keyword(keyword) if is_keyword_value(keyword) => true,
378 TokenKind::Close(_) => true, // a closed delim represents a value on the left
379 _ => false,
380 }
381}
382
383fn is_value_token_right(cooked: &TokenKind) -> bool {
384 match cooked {
385 _ if is_value_lit(cooked) => true,
386 TokenKind::String(StringToken::Interpolated(InterpolatedStart::DollarQuote, _)) => true,
387 TokenKind::Keyword(keyword) if is_keyword_value(keyword) => true,
388 TokenKind::Open(_) => true, // an open delim represents a value on the right
389 _ => false,
390 }
391}
392
393fn effect_no_space(
394 left: &ConcreteToken,
395 whitespace: &str,
396 right: &ConcreteToken,
397 edits: &mut Vec<TextEdit>,
398) {
399 if !whitespace.is_empty() {
400 edits.push(TextEdit::new("", left.span.hi, right.span.lo));
401 }
402}
403
404fn effect_single_space(
405 left: &ConcreteToken,
406 whitespace: &str,
407 right: &ConcreteToken,
408 edits: &mut Vec<TextEdit>,
409) {
410 if whitespace != " " {
411 edits.push(TextEdit::new(" ", left.span.hi, right.span.lo));
412 }
413}
414
415fn effect_trim_comment(left: &ConcreteToken, edits: &mut Vec<TextEdit>, code: &str) {
416 let comment_contents = get_token_contents(code, left);
417 let new_comment_contents = comment_contents.trim_end();
418 if comment_contents != new_comment_contents {
419 edits.push(TextEdit::new(
420 new_comment_contents,
421 left.span.lo,
422 left.span.hi,
423 ));
424 }
425}
426
427fn effect_trim_whitespace(
428 left: &ConcreteToken,
429 whitespace: &str,
430 right: &ConcreteToken,
431 edits: &mut Vec<TextEdit>,
432) {
433 let count_newlines = whitespace.chars().filter(|c| *c == '\n').count();
434 let suffix = match whitespace.rsplit_once('\n') {
435 Some((_, suffix)) => suffix,
436 None => "",
437 };
438
439 let mut new_whitespace = if whitespace.contains("\r\n") {
440 "\r\n".repeat(count_newlines)
441 } else {
442 "\n".repeat(count_newlines)
443 };
444 new_whitespace.push_str(suffix);
445 if whitespace != new_whitespace {
446 edits.push(TextEdit::new(
447 new_whitespace.as_str(),
448 left.span.hi,
449 right.span.lo,
450 ));
451 }
452}
453
454fn effect_correct_indentation(
455 left: &ConcreteToken,
456 whitespace: &str,
457 right: &ConcreteToken,
458 edits: &mut Vec<TextEdit>,
459 indent_level: usize,
460) {
461 let mut count_newlines = whitespace.chars().filter(|c| *c == '\n').count();
462
463 // There should always be at least one newline
464 if count_newlines < 1 {
465 count_newlines = 1;
466 }
467
468 let mut new_whitespace = if whitespace.contains("\r\n") {
469 "\r\n".repeat(count_newlines)
470 } else {
471 "\n".repeat(count_newlines)
472 };
473 new_whitespace.push_str(&make_indent_string(indent_level));
474 if whitespace != new_whitespace {
475 edits.push(TextEdit::new(
476 new_whitespace.as_str(),
477 left.span.hi,
478 right.span.lo,
479 ));
480 }
481}
482
483fn get_token_contents<'a>(code: &'a str, token: &ConcreteToken) -> &'a str {
484 &code[token.span.lo as usize..token.span.hi as usize]
485}
486