microsoft/qdk
Publicmirrored from https://github.com/microsoft/qdkAvailable
compiler/qsc_formatter/src/formatter.rs
485lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | use qsc_data_structures::span::Span; |
| 5 | use 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)] |
| 15 | mod tests; |
| 16 | |
| 17 | #[derive(Debug)] |
| 18 | pub struct TextEdit { |
| 19 | pub new_text: String, |
| 20 | pub span: Span, |
| 21 | } |
| 22 | |
| 23 | impl 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 | |
| 32 | fn 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. |
| 38 | pub 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. |
| 54 | pub 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 | |
| 120 | fn 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 | |
| 300 | fn 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 | |
| 329 | fn is_prefix_with_space(cooked: &TokenKind) -> bool { |
| 330 | matches!(cooked, TokenKind::TildeTildeTilde) |
| 331 | } |
| 332 | |
| 333 | fn is_prefix_without_space(cooked: &TokenKind) -> bool { |
| 334 | matches!( |
| 335 | cooked, |
| 336 | TokenKind::ColonColon | TokenKind::Dot | TokenKind::DotDot |
| 337 | ) |
| 338 | } |
| 339 | |
| 340 | fn is_prefix(cooked: &TokenKind) -> bool { |
| 341 | is_prefix_with_space(cooked) |
| 342 | || is_prefix_without_space(cooked) |
| 343 | || matches!(cooked, TokenKind::DotDotDot) |
| 344 | } |
| 345 | |
| 346 | fn is_suffix(cooked: &TokenKind) -> bool { |
| 347 | matches!(cooked, TokenKind::Bang | TokenKind::Comma) |
| 348 | } |
| 349 | |
| 350 | fn 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 |
| 361 | fn 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 | |
| 373 | fn 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 | |
| 383 | fn 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 | |
| 393 | fn 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 | |
| 404 | fn 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 | |
| 415 | fn 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 | |
| 427 | fn 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 | |
| 454 | fn 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 | |
| 483 | fn 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 | |