fix(sqlserver): complete routine parameters from the typed header

This commit is contained in:
OOMEcho
2026-09-29 17:31:11 +08:00
committed by GitHub
parent a5a4d16992
commit 7a24227e56
2 changed files with 78 additions and 5 deletions
@@ -143,6 +143,36 @@ BEGIN
expect(completionItems(sql)).toEqual(expect.arrayContaining([expect.objectContaining({ label: "@customer_id", type: "variable" }), expect.objectContaining({ label: "@customer_name", type: "variable" })]));
});
it("suggests procedure parameters in the current routine body", () => {
const sql = `CREATE OR ALTER PROCEDURE dbo.find_customer
@customer_id AS INT,
@customer_name NVARCHAR(100) = NULL OUTPUT
AS
BEGIN
SELECT @customer_`;
expect(completionItems(sql)).toEqual(expect.arrayContaining([expect.objectContaining({ label: "@customer_id", type: "variable" }), expect.objectContaining({ label: "@customer_name", type: "variable" })]));
});
it("suggests parenthesized function parameters without collecting body references", () => {
const sql = `ALTER FUNCTION dbo.customer_label(
@customer_id INT,
@separator NVARCHAR(10) = N'-'
)
RETURNS NVARCHAR(200)
AS
BEGIN
DECLARE @label NVARCHAR(200);
SET @phantom = 1;
SELECT @`;
const variables = completionItems(sql)
.filter((item) => item.type === "variable")
.map((item) => item.label);
expect(variables).toEqual(expect.arrayContaining(["@customer_id", "@separator", "@label"]));
expect(variables).not.toContain("@phantom");
});
it("ignores later declarations and declarations inside comments or strings", () => {
const sql = `DECLARE @visible INT;
-- DECLARE @commented INT;
+48 -5
View File
@@ -2651,6 +2651,15 @@ function collectSqlServerLocalVariables(sql: string, cursor: number, options: Sq
const tokens = tokenizeSqlSemantic(batchSql, "sqlserver").filter((token) => token.kind !== "comment" && token.kind !== "string");
const variables: string[] = [];
const seen = new Set<string>();
const addVariable = (variable: string) => {
if (!/^@[A-Za-z_@$#][\w@$#]*$/u.test(variable) || variable.startsWith("@@")) return;
const key = variable.toLowerCase();
if (seen.has(key)) return;
seen.add(key);
variables.push(variable);
};
collectSqlServerRoutineParameters(tokens, addVariable);
for (let index = 0; index < tokens.length; index += 1) {
const declare = tokens[index];
@@ -2668,11 +2677,7 @@ function collectSqlServerLocalVariables(sql: string, cursor: number, options: Sq
if (token.kind === "word" && /^@[A-Za-z_@$#][\w@$#]*$/u.test(token.text) && !token.text.startsWith("@@")) {
const next = tokens[declarationIndex + 1];
if (next && next.text !== "," && next.text !== ";") {
const key = token.text.toLowerCase();
if (!seen.has(key)) {
seen.add(key);
variables.push(token.text);
}
addVariable(token.text);
}
expectVariable = false;
}
@@ -2685,6 +2690,44 @@ function collectSqlServerLocalVariables(sql: string, cursor: number, options: Sq
return variables;
}
function collectSqlServerRoutineParameters(tokens: ReturnType<typeof tokenizeSqlSemantic>, addVariable: (variable: string) => void) {
for (let index = 0; index < tokens.length; index += 1) {
const start = tokens[index];
if (start?.kind !== "word" || (start.normalized !== "create" && start.normalized !== "alter")) continue;
let routineIndex = index + 1;
if (start.normalized === "create" && tokens[routineIndex]?.kind === "word" && tokens[routineIndex]?.normalized === "or" && tokens[routineIndex + 1]?.kind === "word" && tokens[routineIndex + 1]?.normalized === "alter") {
routineIndex += 2;
}
const routine = tokens[routineIndex];
if (routine?.kind !== "word" || !["proc", "procedure", "function"].includes(routine.normalized)) continue;
if (routine.normalized === "function") {
const openingIndex = tokens.findIndex((token, tokenIndex) => tokenIndex > routineIndex && token.kind === "punctuation" && token.text === "(" && token.depth === routine.depth);
if (openingIndex < 0) continue;
const openingDepth = tokens[openingIndex]!.depth;
for (let parameterIndex = openingIndex + 1; parameterIndex < tokens.length; parameterIndex += 1) {
const token = tokens[parameterIndex]!;
if (token.kind === "punctuation" && token.text === ")" && token.depth === openingDepth) break;
if (token.kind === "word" && token.depth === openingDepth + 1) addVariable(token.text);
}
continue;
}
for (let parameterIndex = routineIndex + 1; parameterIndex < tokens.length; parameterIndex += 1) {
const token = tokens[parameterIndex]!;
if (token.depth < routine.depth) break;
if (token.depth !== routine.depth || token.kind !== "word") continue;
if (token.normalized === "as") {
const previous = tokens[parameterIndex - 1];
if (previous?.kind === "word" && previous.text.startsWith("@") && !previous.text.startsWith("@@")) continue;
break;
}
addVariable(token.text);
}
}
}
function sqlTokenStartsLine(sql: string, position: number): boolean {
const lineStart = sql.lastIndexOf("\n", Math.max(0, position - 1)) + 1;
return sql.slice(lineStart, position).trim().length === 0;