chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import type { Diagnostic } from "@codemirror/lint";
|
||||
import type { TableSchema } from "@internal/tsql";
|
||||
import { parseTSQLSelect, SyntaxError, QueryError, validateQuery } from "@internal/tsql";
|
||||
|
||||
/**
|
||||
* Configuration for the TSQL linter
|
||||
*/
|
||||
export interface TSQLLinterConfig {
|
||||
/** Optional schema for validating table/column names */
|
||||
schema?: TableSchema[];
|
||||
/** Delay in milliseconds before running the linter (debouncing) */
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract line and column from a TSQL error message
|
||||
* Error format: "Syntax error at line X:Y: message"
|
||||
*/
|
||||
function parseErrorPosition(message: string): { line: number; column: number } | null {
|
||||
const match = message.match(/at line (\d+):(\d+)/);
|
||||
if (match) {
|
||||
return {
|
||||
line: parseInt(match[1], 10),
|
||||
column: parseInt(match[2], 10),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert line/column to a document position
|
||||
*/
|
||||
function positionToOffset(doc: string, line: number, column: number): number {
|
||||
const lines = doc.split("\n");
|
||||
|
||||
// line is 1-indexed
|
||||
let offset = 0;
|
||||
for (let i = 0; i < line - 1 && i < lines.length; i++) {
|
||||
offset += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
|
||||
return offset + column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the end of a word/token at the given position
|
||||
*/
|
||||
function findTokenEnd(doc: string, start: number): number {
|
||||
let end = start;
|
||||
|
||||
// Scan forward until we hit whitespace or end of string
|
||||
while (end < doc.length && /\S/.test(doc[end])) {
|
||||
end++;
|
||||
}
|
||||
|
||||
// If we didn't move, include at least one character
|
||||
if (end === start) {
|
||||
end = Math.min(start + 1, doc.length);
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TSQL linter function for CodeMirror
|
||||
*
|
||||
* This linter uses the TSQL ANTLR parser to detect syntax errors
|
||||
* and optionally validates against a schema.
|
||||
*
|
||||
* @param config - Linter configuration
|
||||
* @returns A linter function for use with CodeMirror's linter extension
|
||||
*/
|
||||
export function createTSQLLinter(
|
||||
config: TSQLLinterConfig = {}
|
||||
): (view: EditorView) => Diagnostic[] {
|
||||
const { schema = [] } = config;
|
||||
|
||||
return (view: EditorView): Diagnostic[] => {
|
||||
const content = view.state.doc.toString().trim();
|
||||
|
||||
// Return no errors for empty content
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
try {
|
||||
// Try to parse the query
|
||||
const ast = parseTSQLSelect(content);
|
||||
|
||||
// If parsing succeeds and we have a schema, run schema validation
|
||||
if (schema.length > 0) {
|
||||
const validationResult = validateQuery(ast, schema);
|
||||
|
||||
for (const issue of validationResult.issues) {
|
||||
// Map validation severity to CodeMirror diagnostic severity
|
||||
const severity: "error" | "warning" | "info" =
|
||||
issue.severity === "error"
|
||||
? "error"
|
||||
: issue.severity === "warning"
|
||||
? "warning"
|
||||
: "info";
|
||||
|
||||
diagnostics.push({
|
||||
from: 0,
|
||||
to: content.length,
|
||||
severity,
|
||||
message: issue.message,
|
||||
source: "tsql",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
const position = parseErrorPosition(error.message);
|
||||
|
||||
let from: number;
|
||||
let to: number;
|
||||
|
||||
if (position) {
|
||||
from = positionToOffset(content, position.line, position.column);
|
||||
to = findTokenEnd(content, from);
|
||||
} else {
|
||||
// If we can't parse the position, highlight the whole query
|
||||
from = 0;
|
||||
to = content.length;
|
||||
}
|
||||
|
||||
// Clean up the error message
|
||||
let message = error.message;
|
||||
// Remove the "Syntax error at line X:Y: " prefix if present
|
||||
message = message.replace(/^Syntax error at line \d+:\d+:\s*/, "");
|
||||
|
||||
diagnostics.push({
|
||||
from,
|
||||
to,
|
||||
severity: "error",
|
||||
message: message,
|
||||
source: "tsql",
|
||||
});
|
||||
} else if (error instanceof QueryError) {
|
||||
// Schema validation errors don't have position info,
|
||||
// so highlight the whole query
|
||||
diagnostics.push({
|
||||
from: 0,
|
||||
to: content.length,
|
||||
severity: "warning",
|
||||
message: error.message,
|
||||
source: "tsql",
|
||||
});
|
||||
} else if (error instanceof Error) {
|
||||
// Unknown error
|
||||
diagnostics.push({
|
||||
from: 0,
|
||||
to: content.length,
|
||||
severity: "error",
|
||||
message: error.message,
|
||||
source: "tsql",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a TSQL query is valid
|
||||
*
|
||||
* @param query - The query to validate
|
||||
* @returns true if the query is valid, false otherwise
|
||||
*/
|
||||
export function isValidTSQLQuery(query: string): boolean {
|
||||
if (!query.trim()) {
|
||||
return true; // Empty queries are considered valid
|
||||
}
|
||||
|
||||
try {
|
||||
parseTSQLSelect(query);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message for a TSQL query, if any
|
||||
*
|
||||
* @param query - The query to validate
|
||||
* @returns Error message if invalid, null if valid
|
||||
*/
|
||||
export function getTSQLError(query: string): string | null {
|
||||
if (!query.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
parseTSQLSelect(query);
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user