string.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import type {Token} from 'sentry/views/starfish/utils/sqlish/types';
  2. export function string(tokens: Token[]): string {
  3. const accumulator = new StringAccumulator();
  4. let precedingNonWhitespaceToken: Token | undefined = undefined;
  5. let indentation: number = 0; // Tracks the current indent level
  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. indentation += 1;
  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. indentation -= 1;
  33. accumulator.add(INDENTATION.repeat(indentation));
  34. indentationLevels.pop();
  35. }
  36. parenthesisLevel -= 1;
  37. accumulator.add(')');
  38. }
  39. if (typeof token.content === 'string') {
  40. if (token.type === 'Keyword' && NEWLINE_KEYWORDS.has(token.content)) {
  41. if (!accumulator.endsWith(NEWLINE)) {
  42. accumulator.break();
  43. }
  44. accumulator.indent(indentation);
  45. accumulator.add(token.content);
  46. } else if (token.type === 'Whitespace') {
  47. // Convert all whitespace to single spaces
  48. accumulator.space();
  49. } else if (['LeftParenthesis', 'RightParenthesis'].includes(token.type)) {
  50. // Parenthesis contents are appended above, so we can skip them here
  51. } else {
  52. if (accumulator.endsWith(NEWLINE)) {
  53. accumulator.add(INDENTATION.repeat(indentation));
  54. }
  55. accumulator.add(token.content);
  56. }
  57. }
  58. if (token.type !== 'Whitespace') {
  59. precedingNonWhitespaceToken = token;
  60. }
  61. return;
  62. }
  63. tokens.forEach(contentize);
  64. return accumulator.toString();
  65. }
  66. // Keywords that always trigger a newline
  67. const NEWLINE_KEYWORDS = new Set([
  68. 'DELETE',
  69. 'FROM',
  70. 'GROUP',
  71. 'INNER',
  72. 'INSERT',
  73. 'LEFT',
  74. 'LIMIT',
  75. 'OFFSET',
  76. 'ORDER',
  77. 'RETURNING',
  78. 'RIGHT',
  79. 'SELECT',
  80. 'VALUES',
  81. 'WHERE',
  82. ]);
  83. // Keywords that may or may not trigger a newline, but they always trigger a newlines if followed by a parenthesis
  84. const PARENTHESIS_NEWLINE_KEYWORDS = new Set([...NEWLINE_KEYWORDS, ...['IN']]);
  85. class StringAccumulator {
  86. tokens: string[];
  87. constructor() {
  88. this.tokens = [];
  89. }
  90. add(token: string) {
  91. if (!token) {
  92. return;
  93. }
  94. this.tokens.push(token);
  95. }
  96. space() {
  97. this.rtrim();
  98. this.tokens.push(SPACE);
  99. }
  100. break() {
  101. this.rtrim();
  102. this.tokens.push(NEWLINE);
  103. }
  104. indent(count: number = 1) {
  105. this.tokens.push(INDENTATION.repeat(count));
  106. }
  107. rtrim() {
  108. while (this.tokens.at(-1)?.trim() === '') {
  109. this.tokens.pop();
  110. }
  111. }
  112. endsWith(token: string) {
  113. return this.tokens.at(-1) === token;
  114. }
  115. toString() {
  116. return this.tokens.join('').trim();
  117. }
  118. }
  119. const SPACE = ' ';
  120. const INDENTATION = ' ';
  121. const NEWLINE = '\n';