string.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import {StringAccumulator} from 'sentry/views/starfish/utils/sqlish/formatters/stringAccumulator';
  2. import type {Token} from 'sentry/views/starfish/utils/sqlish/types';
  3. export function string(tokens: Token[]): string {
  4. const accumulator = new StringAccumulator();
  5. let precedingNonWhitespaceToken: Token | undefined = undefined;
  6. let parenthesisLevel: number = 0; // Tracks the current parenthesis nesting level
  7. const indentationLevels: number[] = []; // Tracks the parenthesis nesting levels at which we've incremented the indentation
  8. function contentize(token: Token): void {
  9. if (Array.isArray(token.content)) {
  10. token.content.forEach(contentize);
  11. return;
  12. }
  13. if (token.type === 'LeftParenthesis') {
  14. parenthesisLevel += 1;
  15. accumulator.add('(');
  16. // If the previous legible token is a meaningful keyword that triggers a
  17. // newline, increase the current indentation level and note the parenthesis level where this happened
  18. if (
  19. typeof precedingNonWhitespaceToken?.content === 'string' &&
  20. PARENTHESIS_NEWLINE_KEYWORDS.has(precedingNonWhitespaceToken.content)
  21. ) {
  22. accumulator.break();
  23. accumulator.indent();
  24. indentationLevels.push(parenthesisLevel);
  25. }
  26. }
  27. if (token.type === 'RightParenthesis') {
  28. // If this right parenthesis closes a left parenthesis at a level where
  29. // we incremented the indentation, decrement the indentation
  30. if (indentationLevels.at(-1) === parenthesisLevel) {
  31. accumulator.break();
  32. accumulator.unindent();
  33. indentationLevels.pop();
  34. }
  35. parenthesisLevel -= 1;
  36. accumulator.add(')');
  37. }
  38. if (typeof token.content === 'string') {
  39. if (token.type === 'Keyword' && NEWLINE_KEYWORDS.has(token.content)) {
  40. if (!accumulator.lastLine.isEmpty) {
  41. accumulator.break();
  42. }
  43. accumulator.add(token.content);
  44. } else if (token.type === 'Whitespace') {
  45. // Convert all whitespace to single spaces
  46. accumulator.space();
  47. } else if (['LeftParenthesis', 'RightParenthesis'].includes(token.type)) {
  48. // Parenthesis contents are appended above, so we can skip them here
  49. } else {
  50. accumulator.add(token.content);
  51. }
  52. }
  53. if (token.type !== 'Whitespace') {
  54. precedingNonWhitespaceToken = token;
  55. }
  56. return;
  57. }
  58. tokens.forEach(contentize);
  59. return accumulator.toString();
  60. }
  61. // Keywords that always trigger a newline
  62. const NEWLINE_KEYWORDS = new Set([
  63. 'DELETE',
  64. 'FROM',
  65. 'GROUP',
  66. 'INNER',
  67. 'INSERT',
  68. 'LEFT',
  69. 'LIMIT',
  70. 'OFFSET',
  71. 'ORDER',
  72. 'RETURNING',
  73. 'RIGHT',
  74. 'SELECT',
  75. 'VALUES',
  76. 'WHERE',
  77. ]);
  78. // Keywords that may or may not trigger a newline, but they always trigger a newlines if followed by a parenthesis
  79. const PARENTHESIS_NEWLINE_KEYWORDS = new Set([...NEWLINE_KEYWORDS, ...['IN']]);