fix(sqlserver): diagnose DECLARE without data type in routine bodies

This commit is contained in:
zipg authored and GitHub committed 2026-09-21 11:38:04 +08:00
1 parent 2271d840d8
commit 79d6ab64e9
8 files changed
+538 -3

No files matched your search

@@ -136,6 +136,7 @@ import { createHoverSearch, type HoverSearchController } from "@/lib/editor/sqlH
import { lineColumnToOffset, sqlErrorDecorationRange as resolveSqlErrorDecorationRange, sqlErrorSqlMatchesEditor } from "@/lib/sql/sqlDiagnostics";
import { analyzeMysqlRoutineSyntax, supportsMysqlRoutineSyntaxDiagnostics } from "@/lib/sql/mysqlRoutineSyntaxDiagnostics";
import { buildOracleSyntaxDiagnostics } from "@/lib/sql/oracleSyntaxDiagnostics";
import { buildSqlServerRoutineSyntaxDiagnostics } from "@/lib/sql/sqlServerRoutineSyntaxDiagnostics";
import {
DBX_TABLE_REFERENCE_MIME,
DBX_TABLE_REFERENCE_DROP_EVENT,
@@ -217,6 +218,7 @@ import {
isSqlVirtualTableReference,
shouldRunSqlSemanticDiagnostics,
sqlSemanticDiagnosticRangesForViewport,
sqlServerRoutineDefinitionRangesForViewport,
tableReferenceKey,
type SqlSemanticDiagnostic,
} from "@/lib/sql/semantic/diagnostics";
@@ -3940,7 +3942,11 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType, sqlStatementParameterOptions());
}
const diagnosticRanges = sqlSemanticDiagnosticRangesForViewport(sql, visibleRanges, props.databaseType, props.databaseType === "sqlserver" ? undefined : executableStatementRangeCache?.ranges, sqlStatementParameterOptions());
if (diagnosticRanges.length === 0) {
// SQL Server routine batches are excluded from `diagnosticRanges` (see
// `sqlServerRoutineDefinitionRangesForViewport`), so they are recomputed here
// and stay part of the replaced range set below.
const sqlServerRoutineRanges = props.databaseType === "sqlserver" ? sqlServerRoutineDefinitionRangesForViewport(sql, visibleRanges) : [];
if (diagnosticRanges.length === 0 && sqlServerRoutineRanges.length === 0) {
if (!options.preserveOutsideRanges) setSemanticDiagnostics([]);
return;
}
@@ -3953,6 +3959,12 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
return !!diagnosticRange && diagnosticRanges.some((range) => rangesOverlap(diagnosticRange, range));
}),
);
// The analyzer never sees routine batches (the MsSql grammar cannot parse their
// parameter list), so run the token-based routine syntax rules instead of leaving
// a stored procedure without any check at all (dbx#9315).
for (const range of sqlServerRoutineRanges) {
nextDiagnostics.push(...offsetSqlSemanticDiagnostics(buildSqlServerRoutineSyntaxDiagnostics(range.sql, props.databaseType), range, sql));
}
const mysqlRoutineAnalysis = props.databaseType === "mysql" && supportsMysqlRoutineSyntaxDiagnostics(sqlDriverProfile.value) ? analyzeMysqlRoutineSyntax(sql) : null;
if (mysqlRoutineAnalysis) {
nextDiagnostics.push(
@@ -4018,7 +4030,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
}
}
if (options.preserveOutsideRanges) {
replaceSemanticDiagnosticsInRanges(nextDiagnostics, diagnosticRanges, sql);
replaceSemanticDiagnosticsInRanges(nextDiagnostics, [...diagnosticRanges, ...sqlServerRoutineRanges], sql);
} else {
setSemanticDiagnostics(nextDiagnostics.sort(compareSqlSemanticDiagnostics));
}
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
/**
* SQL Server 例程批次的语法诊断接线(#9315)写在 `<script setup>` 内部,
* 无法直接 import,因此沿用仓库既有做法(QueryEditor.cteNavigationWiring.spec.ts)
* 对源码做结构断言。规则本身由 sqlServerRoutineSyntaxDiagnostics.test.ts 覆盖,
* 批次/视口裁剪由 sqlSemanticDiagnostics.test.ts 覆盖,这里只锁定接线契约。
*/
const source = readFileSync(new URL("../QueryEditor.vue", import.meta.url), "utf8");
function normalizeCode(text: string): string {
return text.replace(/\s+/g, "").replace(/,(?=[)\]}])/g, "");
}
const normalizedSource = normalizeCode(source);
function expectSourceToContain(fragment: string): void {
expect(normalizedSource).toContain(normalizeCode(fragment));
}
describe("QueryEditor 的 SQL Server 例程语法诊断接线", () => {
it("例程批次被跳过整个语义范围后仍单独取回", () => {
// sqlSemanticDiagnosticRangesForViewport 对例程批次返回空,所以必须再取一次。
expectSourceToContain('const sqlServerRoutineRanges = props.databaseType === "sqlserver" ? sqlServerRoutineDefinitionRangesForViewport(sql, visibleRanges) : [];');
// 例程批次不能因为普通范围为空就提前 return,否则过程体永远不检查。
expectSourceToContain("if (diagnosticRanges.length === 0 && sqlServerRoutineRanges.length === 0) {");
});
it("例程批次只跑 token 规则,不做引用分析", () => {
expectSourceToContain("nextDiagnostics.push(...offsetSqlSemanticDiagnostics(buildSqlServerRoutineSyntaxDiagnostics(range.sql, props.databaseType), range, sql));");
});
it("增量刷新时把例程批次一并纳入替换范围,避免旧诊断残留", () => {
expectSourceToContain("replaceSemanticDiagnosticsInRanges(nextDiagnostics, [...diagnosticRanges, ...sqlServerRoutineRanges], sql);");
});
});