fix(mongodb): separate formatted shell statements

Closes #5909
This commit is contained in:
t8y2 committed 2026-08-11 13:08:32 +00:00
1 parent 79eb0a21e7
commit 9a16aeaf8b
2 files changed
+116 -5

No files matched your search

@@ -2,6 +2,77 @@ import { describe, expect, it } from "vitest";
import { formatMongoShellText, MAX_MONGO_FORMAT_CHARS } from "@/lib/mongo/mongoFormatter";
describe("mongoFormatter", () => {
it("separates top-level Mongo shell statements", () => {
const query = `db.order_full_link_trace_log.find({
traceId:"master-order:63907695457"
});
db.order_full_link_trace_log.find({}).sort({
masterOrderId:-1
});
db.order_full_link_trace_log.find({
masterOrderId:63907756017
});`;
expect(formatMongoShellText(query)).toBe(`db.order_full_link_trace_log.find({
traceId: "master-order:63907695457"
});
db.order_full_link_trace_log.find({})
.sort({
masterOrderId: -1
});
db.order_full_link_trace_log.find({
masterOrderId: 63907756017
});`);
});
it("normalizes existing statement spacing and remains idempotent", () => {
const expected = "db.first.find({});\n\ndb.second.find({});";
expect(formatMongoShellText("db.first.find({}); db.second.find({});")).toBe(expected);
expect(formatMongoShellText("db.first.find({});\n\n\n db.second.find({});")).toBe(expected);
expect(formatMongoShellText(expected)).toBe(expected);
});
it("leaves a single statement unchanged", () => {
expect(formatMongoShellText("db.items.find({});")).toBe("db.items.find({});");
});
it("does not treat semicolons inside nested syntax or literals as statement boundaries", () => {
const formatted = formatMongoShellText(`db.items.find({
text:"a;b",
pattern:/a;b/,
$where:function(){const ids=[1,2];return ids.length>0;}
});db.logs.find({});`);
expect(formatted).toContain('text: "a;b"');
expect(formatted).toContain("pattern: /a;b/");
expect(formatted).toContain("const ids=[");
expect(formatted).toContain("];return ids.length>0;");
expect(formatted.split("\n\n")).toHaveLength(2);
});
it("keeps trailing comments attached to their statement", () => {
const formatted = formatMongoShellText("db.first.find({}); // keep; this comment\ndb.second.find({}); /* keep; this too */ db.third.find({});");
expect(formatted).toBe("db.first.find({}); // keep; this comment\n\ndb.second.find({}); /* keep; this too */\n\ndb.third.find({});");
});
it("keeps the statement after a standalone line comment executable", () => {
const expected = "db.first.find({});\n\n// comment for next statement\ndb.second.find({});";
expect(formatMongoShellText("db.first.find({});\n// comment for next statement\ndb.second.find({});")).toBe(expected);
expect(formatMongoShellText("db.first.find({});\r\n// comment for next statement\r\ndb.second.find({});")).toBe(expected);
expect(formatMongoShellText(expected)).toBe(expected);
});
it("does not add a line break after a final line comment", () => {
expect(formatMongoShellText("db.first.find({}); // final comment")).toBe("db.first.find({}); // final comment");
});
it("formats documents with many short fields without repeated whole-output rewrites", () => {
const fields = Array.from({ length: 20_000 }, (_, index) => `"field${index}":${index}`).join(",");
const query = `db.items.insert({${fields}});`;
+45 -5
View File
@@ -9,6 +9,7 @@ interface FormatState {
indentLevel: number;
atLineStart: boolean;
pendingSpace: boolean;
pendingStatementBreak: boolean;
chainIndent: boolean;
pendingChainCall: boolean;
stack: Array<{ char: string; expanded: boolean; chainCall?: boolean }>;
@@ -23,12 +24,37 @@ export function formatMongoShellText(text: string, settings: Partial<SqlFormatte
}
const indentUnit = settings.useTabs ? "\t" : " ".repeat(settings.tabWidth ?? DEFAULT_SQL_FORMATTER_SETTINGS.tabWidth);
const state: FormatState = { out: [], lastChar: "", lastNonWhitespace: "", indentLevel: 0, atLineStart: true, pendingSpace: false, chainIndent: false, pendingChainCall: false, stack: [] };
const state: FormatState = { out: [], lastChar: "", lastNonWhitespace: "", indentLevel: 0, atLineStart: true, pendingSpace: false, pendingStatementBreak: false, chainIndent: false, pendingChainCall: false, stack: [] };
for (let index = 0; index < text.length; index++) {
const char = text[index] ?? "";
const next = text[index + 1] ?? "";
if (state.pendingStatementBreak) {
if (/\s/.test(char)) {
if (char === "\n" || char === "\r") separateStatements(state, indentUnit);
else state.pendingSpace = !state.atLineStart;
continue;
}
if (char === "/" && next === "/") {
const comment = readLineComment(text, index);
appendToken(state, comment.value, indentUnit);
if (comment.hasLineBreak) newline(state, indentUnit);
index = comment.end;
continue;
}
if (char === "/" && next === "*") {
const comment = readBlockComment(text, index);
appendToken(state, comment.value, indentUnit);
index = comment.end;
continue;
}
separateStatements(state, indentUnit);
}
if (char === '"' || char === "'" || char === "`") {
const literal = readQuotedLiteral(text, index, char);
appendToken(state, literal.value, indentUnit);
@@ -39,6 +65,7 @@ export function formatMongoShellText(text: string, settings: Partial<SqlFormatte
if (char === "/" && next === "/") {
const comment = readLineComment(text, index);
appendToken(state, comment.value, indentUnit);
if (comment.hasLineBreak) newline(state, indentUnit);
index = comment.end;
continue;
}
@@ -110,6 +137,12 @@ export function formatMongoShellText(text: string, settings: Partial<SqlFormatte
continue;
}
if (char === ";" && state.stack.length === 0) {
appendToken(state, char, indentUnit);
state.pendingStatementBreak = true;
continue;
}
appendToken(state, char, indentUnit);
}
@@ -148,6 +181,12 @@ function newline(state: FormatState, indentUnit: string, indentDelta = 0) {
void indentUnit;
}
function separateStatements(state: FormatState, indentUnit: string) {
newline(state, indentUnit);
appendOutput(state, "\n");
state.pendingStatementBreak = false;
}
function shouldInsertPendingSpace(previous: string, token: string): boolean {
if (!previous) return false;
if ([".", "(", "[", "{"].includes(previous)) return false;
@@ -194,10 +233,11 @@ function readQuotedLiteral(text: string, start: number, quote: string): { value:
return { value: text.slice(start), end: text.length - 1 };
}
function readLineComment(text: string, start: number): { value: string; end: number } {
const end = text.indexOf("\n", start);
if (end < 0) return { value: text.slice(start), end: text.length - 1 };
return { value: text.slice(start, end), end: end - 1 };
function readLineComment(text: string, start: number): { value: string; end: number; hasLineBreak: boolean } {
let end = start + 2;
while (end < text.length && text[end] !== "\n" && text[end] !== "\r") end++;
if (end === text.length) return { value: text.slice(start), end: text.length - 1, hasLineBreak: false };
return { value: text.slice(start, end), end: end - 1, hasLineBreak: true };
}
function readBlockComment(text: string, start: number): { value: string; end: number } {