string.ts 2.9 KB

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