string.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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. accumulator.add('');
  52. } else {
  53. if (accumulator.endsWith(NEWLINE)) {
  54. accumulator.add(INDENTATION.repeat(indentation));
  55. }
  56. accumulator.add(token.content);
  57. }
  58. }
  59. if (token.type !== 'Whitespace') {
  60. precedingNonWhitespaceToken = token;
  61. }
  62. return;
  63. }
  64. tokens.forEach(contentize);
  65. return accumulator.toString();
  66. }
  67. // Keywords that always trigger a newline
  68. const NEWLINE_KEYWORDS = new Set([
  69. 'DELETE',
  70. 'FROM',
  71. 'GROUP',
  72. 'INNER',
  73. 'INSERT',
  74. 'LEFT',
  75. 'LIMIT',
  76. 'OFFSET',
  77. 'ORDER',
  78. 'RETURNING',
  79. 'RIGHT',
  80. 'SELECT',
  81. 'VALUES',
  82. 'WHERE',
  83. ]);
  84. // Keywords that may or may not trigger a newline, but they always trigger a newlines if followed by a parenthesis
  85. const PARENTHESIS_NEWLINE_KEYWORDS = new Set([...NEWLINE_KEYWORDS, ...['IN']]);
  86. class StringAccumulator {
  87. tokens: string[];
  88. constructor() {
  89. this.tokens = [];
  90. }
  91. add(token: string) {
  92. if (!token) {
  93. return;
  94. }
  95. this.tokens.push(token);
  96. }
  97. space() {
  98. this.tokens.push(SPACE);
  99. }
  100. break() {
  101. if (this.tokens.at(-1) === SPACE) {
  102. this.tokens.pop();
  103. }
  104. this.tokens.push(NEWLINE);
  105. }
  106. indent(count: number = 1) {
  107. this.tokens.push(INDENTATION.repeat(count));
  108. }
  109. endsWith(token: string) {
  110. return this.tokens.at(-1) === token;
  111. }
  112. toString() {
  113. return this.tokens.join('').trim();
  114. }
  115. }
  116. const SPACE = ' ';
  117. const INDENTATION = ' ';
  118. const NEWLINE = '\n';