mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(mongo): stop misattributing helper lines in command diagnostics
Closes #11279
This commit is contained in:
@@ -1455,25 +1455,26 @@ function splitMongoSegmentAtSoftStarts(segment: MongoTextRange): MongoTextRange[
|
||||
for (let index = 1; index < boundaries.length; index += 1) {
|
||||
const boundary = boundaries[index] ?? 0;
|
||||
const candidate = trimMongoOuterCommentRange(segment.text, start, boundary);
|
||||
// Only accept newline-based splitting when every slice is a valid command;
|
||||
// otherwise keep the original text intact and let normal parsing reject it.
|
||||
if (!candidate || !parseMongoCommand(candidate.text)) return [segment];
|
||||
segments.push({
|
||||
from: segment.from + candidate.from,
|
||||
to: segment.from + candidate.to,
|
||||
text: candidate.text,
|
||||
});
|
||||
if (candidate) pushMongoSlice(segments, segment, candidate);
|
||||
start = boundary;
|
||||
}
|
||||
|
||||
const last = trimMongoOuterCommentRange(segment.text, start, segment.text.length);
|
||||
if (!last || !parseMongoCommand(last.text)) return [segment];
|
||||
segments.push({
|
||||
from: segment.from + last.from,
|
||||
to: segment.from + last.to,
|
||||
text: last.text,
|
||||
});
|
||||
return segments;
|
||||
if (last) pushMongoSlice(segments, segment, last);
|
||||
// Splitting is an improvement only when at least one slice really is a command;
|
||||
// otherwise keep the text intact so the normal parser reports the failure once
|
||||
// instead of once per line.
|
||||
return segments.some((slice) => isMongoCommandText(slice.text)) && segments.length > 1 ? segments : [segment];
|
||||
}
|
||||
|
||||
function pushMongoSlice(segments: MongoTextRange[], parent: MongoTextRange, slice: MongoTextRange): void {
|
||||
segments.push({ from: parent.from + slice.from, to: parent.from + slice.to, text: slice.text });
|
||||
}
|
||||
|
||||
/** Does this text start like a shell command (`db…`, `use …`, `show dbs`)? */
|
||||
export function isMongoCommandText(text: string): boolean {
|
||||
const trimmed = text.trimStart();
|
||||
return isMongoCommandLineStart(trimmed, 0) || /^rs[.\[]|^sh[.\[]/i.test(trimmed);
|
||||
}
|
||||
|
||||
function mongoTopLevelCommandLineStarts(segment: string): number[] {
|
||||
@@ -1485,15 +1486,26 @@ function mongoTopLevelCommandLineStarts(segment: string): number[] {
|
||||
let blockComment = false;
|
||||
let lineStart = 0;
|
||||
let firstNonWhitespaceOnLine = -1;
|
||||
// Does the line we just finished close a statement? Used so a JS helper line
|
||||
// between two commands (`var ids = [...]`) still starts its own statement instead
|
||||
// of being glued onto the command above it.
|
||||
let lineHadCode = false;
|
||||
let closesStatement = false;
|
||||
let afterClosedStatement = false;
|
||||
|
||||
for (let i = 0; i < segment.length; i += 1) {
|
||||
const char = segment[i] ?? "";
|
||||
const next = segment[i + 1] ?? "";
|
||||
|
||||
if (char === "\n") {
|
||||
// A lone CR is a line break too (pasted shell history, old Mac files); CRLF is
|
||||
// handled by the LF branch once the CR falls inside the same line.
|
||||
if (char === "\n" || (char === "\r" && next !== "\n")) {
|
||||
if (lineComment) lineComment = false;
|
||||
afterClosedStatement = depth === 0 && lineHadCode && closesStatement;
|
||||
lineStart = i + 1;
|
||||
firstNonWhitespaceOnLine = -1;
|
||||
lineHadCode = false;
|
||||
closesStatement = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1530,16 +1542,31 @@ function mongoTopLevelCommandLineStarts(segment: string): number[] {
|
||||
|
||||
if (char === '"' || char === "'" || char === "`") {
|
||||
if (firstNonWhitespaceOnLine === -1 && !/\s/.test(char)) firstNonWhitespaceOnLine = i;
|
||||
lineHadCode = true;
|
||||
closesStatement = false;
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/\s/.test(char)) continue;
|
||||
|
||||
if (char === "{" || char === "[" || char === "(") depth += 1;
|
||||
else if ((char === "}" || char === "]" || char === ")") && depth > 0) depth -= 1;
|
||||
else if (char === "}" || char === "]" || char === ")") {
|
||||
if (depth > 0) depth -= 1;
|
||||
closesStatement = depth === 0;
|
||||
} else if (char === ";" && depth === 0) {
|
||||
closesStatement = true;
|
||||
} else {
|
||||
closesStatement = false;
|
||||
}
|
||||
lineHadCode = true;
|
||||
|
||||
if (firstNonWhitespaceOnLine === -1 && !/\s/.test(char)) {
|
||||
firstNonWhitespaceOnLine = i;
|
||||
if (depth === 0 && char !== "." && isMongoCommandLineStart(segment, i)) starts.push(i);
|
||||
if (depth === 0 && char !== "." && (isMongoCommandLineStart(segment, i) || afterClosedStatement)) {
|
||||
starts.push(i);
|
||||
afterClosedStatement = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import type { SqlTextSpan } from "@/types/database";
|
||||
import type { SqlSemanticDiagnostic } from "@/lib/sql/semantic/diagnostics";
|
||||
import { findMatchingParen, hasUnclosedMongoDelimiters, trimMongoOuterComments } from "@dbx-app/mongo-shell";
|
||||
import { bulkWriteFilters, describeMongoCommandParseFailure, mongoFilterIsEffectivelyUnbounded, parseMongoCommand, splitMongoCommandTextRanges, type MongoCommand, type MongoTextRange } from "@/lib/mongo/mongoShellCommand";
|
||||
import { bulkWriteFilters, describeMongoCommandParseFailure, isMongoCommandText, mongoFilterIsEffectivelyUnbounded, parseMongoCommand, splitMongoCommandTextRanges, type MongoCommand, type MongoTextRange } from "@/lib/mongo/mongoShellCommand";
|
||||
|
||||
const UNCLOSED_MESSAGE = "MongoDB command has unclosed parentheses, brackets, braces, or strings.";
|
||||
|
||||
@@ -24,10 +24,15 @@ export function buildMongoSyntaxDiagnostics(source: string, cursor = -1): SqlSem
|
||||
const diagnostics: SqlSemanticDiagnostic[] = [];
|
||||
const segments = splitMongoCommandTextRanges(source);
|
||||
const current = segmentAtCursor(segments, cursor, source.length);
|
||||
// Shell users keep JS helper statements between commands (`var ids = [...]` feeding a
|
||||
// later `$in`). Those lines are not commands dbx can validate, and trying to diagnose
|
||||
// them is what made the command above them carry a bogus "Unexpected text after find(...)".
|
||||
const hasCommand = segments.some((segment) => isMongoCommandText(trimMongoOuterComments(segment.text)));
|
||||
|
||||
for (const segment of segments) {
|
||||
const text = trimMongoOuterComments(segment.text).trim().replace(/;$/, "").trim();
|
||||
if (!text) continue;
|
||||
if (hasCommand && !isMongoCommandText(text)) continue;
|
||||
|
||||
if (hasUnclosedMongoDelimiters(text)) {
|
||||
// A command still being typed under the cursor is not an error yet.
|
||||
|
||||
@@ -19,6 +19,26 @@ test("valid commands and comments produce no diagnostics", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("a JS helper line between commands does not blame the command above it", () => {
|
||||
// Shell users build a big `$in` list in a variable, and the editor must not turn
|
||||
// that into "Unexpected text after find(...)" on the previous, correct command.
|
||||
const helper = underlined('db.getCollection("a").find({ x: 1 })\nvar ids = [1, 2, 3]\ndb.getCollection("b").find({ _id: { $in: ids } })');
|
||||
assert.ok(!helper.some((d) => /Unexpected text after/.test(d.message)), JSON.stringify(helper));
|
||||
assert.equal(helper.length, 1, JSON.stringify(helper));
|
||||
// What is left is the honest complaint about the third line: dbx cannot evaluate `ids`.
|
||||
assert.match(helper[0]!.message, /filter argument of find\(\) is not a valid document/);
|
||||
assert.equal(helper[0]!.text, 'db.getCollection("b").find');
|
||||
|
||||
// A `print()` between two commands is not a command either, so both stay clean.
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics('db.a.find({ x: 1 })\nprint("done")\ndb.b.find({ y: 2 })'), []);
|
||||
// Same for a lone CR separating commands (pasted shell history).
|
||||
assert.deepEqual(buildMongoSyntaxDiagnostics('db.a.find({ x: 1 })\rdb.b.find({ y: 2 })'), []);
|
||||
|
||||
// A real error on a later line is still reported, on that line and about that command.
|
||||
const [late] = underlined("db.a.find({})\nvar ids = [1]\ndb.b.foobar({})");
|
||||
assert.match(late!.message, /Collection method foobar\(\) is not supported/);
|
||||
});
|
||||
|
||||
test("underlines the offending value constructor and names the argument", () => {
|
||||
const [single] = underlined('db.reports.find({id: Foo("x")})');
|
||||
assert.equal(single?.severity, "error");
|
||||
|
||||
Reference in New Issue
Block a user