feat(recipe-studio, validators): extend OXC validator with code shape support and integrate into recipe studio
This commit is contained in:
parent
f118216898
commit
93063c3212
10 changed files with 379 additions and 116 deletions
|
|
@ -20,6 +20,7 @@ _OXC_LANG_TO_NODE_LANG = {
|
|||
"tsx": "tsx",
|
||||
}
|
||||
_OXC_VALIDATION_MODES = {"syntax", "lint", "syntax+lint"}
|
||||
_OXC_CODE_SHAPES = {"auto", "module", "snippet"}
|
||||
|
||||
_OXC_TOOL_DIR = Path(__file__).resolve().parent / "oxc-validator"
|
||||
_OXC_RUNNER_PATH = _OXC_TOOL_DIR / "validate.mjs"
|
||||
|
|
@ -33,6 +34,7 @@ class OxcLocalCallableValidatorSpec:
|
|||
batch_size: int
|
||||
code_lang: str
|
||||
validation_mode: str
|
||||
code_shape: str
|
||||
|
||||
|
||||
def split_oxc_local_callable_validators(
|
||||
|
|
@ -83,6 +85,7 @@ def register_oxc_local_callable_validators(
|
|||
validation_function = _build_oxc_validation_function(
|
||||
spec.code_lang,
|
||||
spec.validation_mode,
|
||||
spec.code_shape,
|
||||
)
|
||||
builder.add_column(
|
||||
ValidationColumnConfig(
|
||||
|
|
@ -129,7 +132,7 @@ def _parse_oxc_spec(
|
|||
if not target_columns:
|
||||
return None
|
||||
|
||||
code_lang, validation_mode = _parse_oxc_validation_marker(fn_name)
|
||||
code_lang, validation_mode, code_shape = _parse_oxc_validation_marker(fn_name)
|
||||
batch_size = _parse_batch_size(column.get("batch_size"))
|
||||
drop = bool(column.get("drop") is True)
|
||||
|
||||
|
|
@ -140,6 +143,7 @@ def _parse_oxc_spec(
|
|||
batch_size=batch_size,
|
||||
code_lang=code_lang,
|
||||
validation_mode=validation_mode,
|
||||
code_shape=code_shape,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -151,27 +155,29 @@ def _parse_batch_size(value: Any) -> int:
|
|||
return parsed if parsed >= 1 else 10
|
||||
|
||||
|
||||
def _parse_oxc_validation_marker(fn_name: str) -> tuple[str, str]:
|
||||
def _parse_oxc_validation_marker(fn_name: str) -> tuple[str, str, str]:
|
||||
marker = f"{OXC_VALIDATION_FN_MARKER}:"
|
||||
if not fn_name.startswith(marker):
|
||||
return "javascript", "syntax"
|
||||
return "javascript", "syntax", "auto"
|
||||
suffix = fn_name[len(marker) :]
|
||||
parts = [part.strip() for part in suffix.split(":") if part.strip()]
|
||||
if len(parts) < 2:
|
||||
return "javascript", "syntax"
|
||||
return "javascript", "syntax", "auto"
|
||||
code_lang = parts[0] if parts[0] in _OXC_LANG_TO_NODE_LANG else "javascript"
|
||||
mode = parts[1] if parts[1] in _OXC_VALIDATION_MODES else "syntax"
|
||||
return code_lang, mode
|
||||
code_shape = parts[2] if len(parts) >= 3 and parts[2] in _OXC_CODE_SHAPES else "auto"
|
||||
return code_lang, mode, code_shape
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _build_oxc_validation_function(lang: str, validation_mode: str):
|
||||
def _build_oxc_validation_function(lang: str, validation_mode: str, code_shape: str):
|
||||
node_lang = _OXC_LANG_TO_NODE_LANG.get(lang, "js")
|
||||
mode = (
|
||||
validation_mode
|
||||
if validation_mode in _OXC_VALIDATION_MODES
|
||||
else "syntax"
|
||||
)
|
||||
normalized_code_shape = code_shape if code_shape in _OXC_CODE_SHAPES else "auto"
|
||||
|
||||
def _validator(df):
|
||||
import pandas as pd # imported lazily for local callable runtime
|
||||
|
|
@ -190,6 +196,7 @@ def _build_oxc_validation_function(lang: str, validation_mode: str):
|
|||
results = _run_oxc_batch(
|
||||
node_lang=node_lang,
|
||||
validation_mode=mode,
|
||||
code_shape=normalized_code_shape,
|
||||
code_values=code_values,
|
||||
)
|
||||
if len(results) != row_count:
|
||||
|
|
@ -199,7 +206,9 @@ def _build_oxc_validation_function(lang: str, validation_mode: str):
|
|||
)
|
||||
return pd.DataFrame(results)
|
||||
|
||||
_validator.__name__ = f"{OXC_VALIDATION_FN_MARKER}_{node_lang}_{mode.replace('+', '_')}"
|
||||
_validator.__name__ = (
|
||||
f"{OXC_VALIDATION_FN_MARKER}_{node_lang}_{mode.replace('+', '_')}_{normalized_code_shape}"
|
||||
)
|
||||
return _validator
|
||||
|
||||
|
||||
|
|
@ -207,6 +216,7 @@ def _run_oxc_batch(
|
|||
*,
|
||||
node_lang: str,
|
||||
validation_mode: str,
|
||||
code_shape: str,
|
||||
code_values: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
if not _OXC_RUNNER_PATH.exists():
|
||||
|
|
@ -218,6 +228,7 @@ def _run_oxc_batch(
|
|||
payload = {
|
||||
"lang": node_lang,
|
||||
"mode": validation_mode,
|
||||
"code_shape": code_shape,
|
||||
"codes": code_values,
|
||||
}
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ const LANG_TO_EXT = {
|
|||
};
|
||||
|
||||
const VALIDATION_MODES = new Set(["syntax", "lint", "syntax+lint"]);
|
||||
const CODE_SHAPES = new Set(["auto", "module", "snippet"]);
|
||||
const SNIPPET_PREFIX = "(() => {\n";
|
||||
const SNIPPET_SUFFIX = "\n})();\nexport {};\n";
|
||||
const TOOL_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function mapLang(value) {
|
||||
|
|
@ -40,6 +43,14 @@ function mapMode(value) {
|
|||
return "syntax";
|
||||
}
|
||||
|
||||
function mapCodeShape(value) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
if (CODE_SHAPES.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
function parseFileIndex(filePath) {
|
||||
if (typeof filePath !== "string") {
|
||||
return null;
|
||||
|
|
@ -52,6 +63,52 @@ function parseFileIndex(filePath) {
|
|||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function toCodeString(code) {
|
||||
return typeof code === "string" ? code : String(code ?? "");
|
||||
}
|
||||
|
||||
function makeValidationEntry({ code, index, lang, codeShape }) {
|
||||
const source = toCodeString(code);
|
||||
if (codeShape === "snippet") {
|
||||
return {
|
||||
index,
|
||||
lang,
|
||||
code: `${SNIPPET_PREFIX}${source}${SNIPPET_SUFFIX}`,
|
||||
offset: SNIPPET_PREFIX.length,
|
||||
};
|
||||
}
|
||||
return {
|
||||
index,
|
||||
lang,
|
||||
code: source,
|
||||
offset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function shiftOffset(value, offset) {
|
||||
if (!Number.isInteger(value)) {
|
||||
return null;
|
||||
}
|
||||
const shifted = value - offset;
|
||||
return shifted >= 0 ? shifted : null;
|
||||
}
|
||||
|
||||
function remapDiagnosticOffsets(diagnostic, offset) {
|
||||
if (!diagnostic || typeof diagnostic !== "object" || offset <= 0) {
|
||||
return diagnostic;
|
||||
}
|
||||
return {
|
||||
...diagnostic,
|
||||
labels: Array.isArray(diagnostic.labels)
|
||||
? diagnostic.labels.map((label) => ({
|
||||
...label,
|
||||
start: shiftOffset(label.start, offset),
|
||||
end: shiftOffset(label.end, offset),
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeParserError(error) {
|
||||
if (typeof error === "string") {
|
||||
return {
|
||||
|
|
@ -160,48 +217,140 @@ function makeResult({
|
|||
};
|
||||
}
|
||||
|
||||
function validateSyntaxOne({ code, lang, index }) {
|
||||
const ext = LANG_TO_EXT[lang] ?? "js";
|
||||
const filename = `snippet_${index}.${ext}`;
|
||||
const source = typeof code === "string" ? code : String(code ?? "");
|
||||
function syntaxResultFromErrors(errors) {
|
||||
const first = errors[0] ?? null;
|
||||
return makeResult({
|
||||
isValid: errors.length === 0,
|
||||
errorCount: errors.length,
|
||||
warningCount: 0,
|
||||
message: errors.slice(0, 3).map((error) => error.message).join(" | "),
|
||||
severity: first ? first.severity : null,
|
||||
labels: first ? first.labels : [],
|
||||
codeframe: first ? first.codeframe : null,
|
||||
});
|
||||
}
|
||||
|
||||
function runSyntaxParse(entry) {
|
||||
const ext = LANG_TO_EXT[entry.lang] ?? "js";
|
||||
const filename = `snippet_${entry.index}.${ext}`;
|
||||
try {
|
||||
const parsed = parseSync(filename, source, {
|
||||
lang,
|
||||
const parsed = parseSync(filename, entry.code, {
|
||||
lang: entry.lang,
|
||||
sourceType: "module",
|
||||
showSemanticErrors: true,
|
||||
});
|
||||
const errors = Array.isArray(parsed?.errors)
|
||||
? parsed.errors.map(normalizeParserError).filter(Boolean)
|
||||
? parsed.errors
|
||||
.map(normalizeParserError)
|
||||
.filter(Boolean)
|
||||
.map((error) => remapDiagnosticOffsets(error, entry.offset))
|
||||
: [];
|
||||
const first = errors[0] ?? null;
|
||||
return makeResult({
|
||||
isValid: errors.length === 0,
|
||||
errorCount: errors.length,
|
||||
warningCount: 0,
|
||||
message: errors.slice(0, 3).map((error) => error.message).join(" | "),
|
||||
severity: first ? first.severity : null,
|
||||
labels: first ? first.labels : [],
|
||||
codeframe: first ? first.codeframe : null,
|
||||
});
|
||||
return errors;
|
||||
} catch (error) {
|
||||
const normalized = normalizeParserError(error);
|
||||
return makeResult({
|
||||
isValid: false,
|
||||
errorCount: 1,
|
||||
warningCount: 0,
|
||||
message: normalized.message,
|
||||
severity: normalized.severity,
|
||||
labels: normalized.labels,
|
||||
codeframe: normalized.codeframe,
|
||||
});
|
||||
return [
|
||||
remapDiagnosticOffsets(
|
||||
normalizeParserError(error),
|
||||
entry.offset,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackLintResults(indexedCodes, message) {
|
||||
function pickPreferredErrorList(firstErrors, secondErrors) {
|
||||
if (secondErrors.length < firstErrors.length) {
|
||||
return secondErrors;
|
||||
}
|
||||
return firstErrors;
|
||||
}
|
||||
|
||||
function validateSyntaxOne({ code, lang, index, codeShape }) {
|
||||
if (codeShape !== "auto") {
|
||||
const lintEntry = makeValidationEntry({
|
||||
code,
|
||||
index,
|
||||
lang,
|
||||
codeShape,
|
||||
});
|
||||
const errors = runSyntaxParse(lintEntry);
|
||||
return {
|
||||
result: syntaxResultFromErrors(errors),
|
||||
lintEntry,
|
||||
};
|
||||
}
|
||||
|
||||
const moduleEntry = makeValidationEntry({
|
||||
code,
|
||||
index,
|
||||
lang,
|
||||
codeShape: "module",
|
||||
});
|
||||
const moduleErrors = runSyntaxParse(moduleEntry);
|
||||
if (moduleErrors.length === 0) {
|
||||
return {
|
||||
result: syntaxResultFromErrors(moduleErrors),
|
||||
lintEntry: moduleEntry,
|
||||
};
|
||||
}
|
||||
|
||||
const snippetEntry = makeValidationEntry({
|
||||
code,
|
||||
index,
|
||||
lang,
|
||||
codeShape: "snippet",
|
||||
});
|
||||
const snippetErrors = runSyntaxParse(snippetEntry);
|
||||
if (snippetErrors.length === 0) {
|
||||
return {
|
||||
result: syntaxResultFromErrors(snippetErrors),
|
||||
lintEntry: snippetEntry,
|
||||
};
|
||||
}
|
||||
|
||||
const chosenErrors = pickPreferredErrorList(moduleErrors, snippetErrors);
|
||||
const lintEntry = chosenErrors === snippetErrors ? snippetEntry : moduleEntry;
|
||||
return {
|
||||
result: syntaxResultFromErrors(chosenErrors),
|
||||
lintEntry,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveLintEntry({ code, lang, index, codeShape }) {
|
||||
if (codeShape !== "auto") {
|
||||
return makeValidationEntry({
|
||||
code,
|
||||
index,
|
||||
lang,
|
||||
codeShape,
|
||||
});
|
||||
}
|
||||
|
||||
const moduleEntry = makeValidationEntry({
|
||||
code,
|
||||
index,
|
||||
lang,
|
||||
codeShape: "module",
|
||||
});
|
||||
if (runSyntaxParse(moduleEntry).length === 0) {
|
||||
return moduleEntry;
|
||||
}
|
||||
|
||||
const snippetEntry = makeValidationEntry({
|
||||
code,
|
||||
index,
|
||||
lang,
|
||||
codeShape: "snippet",
|
||||
});
|
||||
if (runSyntaxParse(snippetEntry).length === 0) {
|
||||
return snippetEntry;
|
||||
}
|
||||
|
||||
return moduleEntry;
|
||||
}
|
||||
|
||||
function fallbackLintResults(entries, message) {
|
||||
return new Map(
|
||||
indexedCodes.map((item) => [
|
||||
item.index,
|
||||
entries.map((entry) => [
|
||||
entry.index,
|
||||
makeResult({
|
||||
isValid: false,
|
||||
errorCount: 1,
|
||||
|
|
@ -213,18 +362,18 @@ function fallbackLintResults(indexedCodes, message) {
|
|||
);
|
||||
}
|
||||
|
||||
function runLintBatch(indexedCodes, lang) {
|
||||
if (indexedCodes.length === 0) {
|
||||
function runLintBatch(entries) {
|
||||
if (entries.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const ext = LANG_TO_EXT[lang] ?? "js";
|
||||
const entryByIndex = new Map(entries.map((entry) => [entry.index, entry]));
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "oxlint-"));
|
||||
try {
|
||||
for (const item of indexedCodes) {
|
||||
const filePath = join(tempDir, `snippet_${item.index}.${ext}`);
|
||||
const source = typeof item.code === "string" ? item.code : String(item.code ?? "");
|
||||
writeFileSync(filePath, source, "utf8");
|
||||
for (const entry of entries) {
|
||||
const ext = LANG_TO_EXT[entry.lang] ?? "js";
|
||||
const filePath = join(tempDir, `snippet_${entry.index}.${ext}`);
|
||||
writeFileSync(filePath, entry.code, "utf8");
|
||||
}
|
||||
|
||||
const oxlintBin = join(TOOL_DIR, "node_modules", ".bin", "oxlint");
|
||||
|
|
@ -234,7 +383,7 @@ function runLintBatch(indexedCodes, lang) {
|
|||
});
|
||||
if (exec.error) {
|
||||
return fallbackLintResults(
|
||||
indexedCodes,
|
||||
entries,
|
||||
`oxlint execution failed: ${exec.error.message}`,
|
||||
);
|
||||
}
|
||||
|
|
@ -242,7 +391,7 @@ function runLintBatch(indexedCodes, lang) {
|
|||
if (!stdout) {
|
||||
const stderr = String(exec.stderr || "").trim();
|
||||
return fallbackLintResults(
|
||||
indexedCodes,
|
||||
entries,
|
||||
stderr || "oxlint returned empty output",
|
||||
);
|
||||
}
|
||||
|
|
@ -251,7 +400,7 @@ function runLintBatch(indexedCodes, lang) {
|
|||
try {
|
||||
parsed = JSON.parse(stdout);
|
||||
} catch {
|
||||
return fallbackLintResults(indexedCodes, "oxlint JSON parse failed");
|
||||
return fallbackLintResults(entries, "oxlint JSON parse failed");
|
||||
}
|
||||
|
||||
const rawDiagnostics = Array.isArray(parsed?.diagnostics)
|
||||
|
|
@ -260,10 +409,6 @@ function runLintBatch(indexedCodes, lang) {
|
|||
const byIndex = new Map();
|
||||
|
||||
for (const diag of rawDiagnostics) {
|
||||
const normalized = normalizeLintDiagnostic(diag);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
const filenameRaw =
|
||||
typeof diag?.filename === "string" ? diag.filename : "";
|
||||
const filename = filenameRaw.startsWith("file://")
|
||||
|
|
@ -273,14 +418,20 @@ function runLintBatch(indexedCodes, lang) {
|
|||
if (index === null) {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeLintDiagnostic(diag);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
const entry = entryByIndex.get(index);
|
||||
const remapped = remapDiagnosticOffsets(normalized, entry?.offset ?? 0);
|
||||
const list = byIndex.get(index) ?? [];
|
||||
list.push(normalized);
|
||||
list.push(remapped);
|
||||
byIndex.set(index, list);
|
||||
}
|
||||
|
||||
const results = new Map();
|
||||
for (const item of indexedCodes) {
|
||||
const diagnostics = byIndex.get(item.index) ?? [];
|
||||
for (const entry of entries) {
|
||||
const diagnostics = byIndex.get(entry.index) ?? [];
|
||||
const errorDiagnostics = diagnostics.filter(
|
||||
(diag) => diag.severity === "error",
|
||||
);
|
||||
|
|
@ -291,7 +442,7 @@ function runLintBatch(indexedCodes, lang) {
|
|||
const messageSource =
|
||||
errorDiagnostics.length > 0 ? errorDiagnostics : warningDiagnostics;
|
||||
results.set(
|
||||
item.index,
|
||||
entry.index,
|
||||
makeResult({
|
||||
isValid: errorDiagnostics.length === 0,
|
||||
errorCount: errorDiagnostics.length,
|
||||
|
|
@ -308,7 +459,7 @@ function runLintBatch(indexedCodes, lang) {
|
|||
}
|
||||
return results;
|
||||
} catch (error) {
|
||||
return fallbackLintResults(indexedCodes, `oxlint execution failed: ${error}`);
|
||||
return fallbackLintResults(entries, `oxlint execution failed: ${error}`);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -326,16 +477,21 @@ function readStdin() {
|
|||
});
|
||||
}
|
||||
|
||||
function runValidation({ codes, lang, mode }) {
|
||||
function runValidation({ codes, lang, mode, codeShape }) {
|
||||
if (mode === "syntax") {
|
||||
return codes.map((code, index) => validateSyntaxOne({ code, lang, index }));
|
||||
return codes.map((code, index) =>
|
||||
validateSyntaxOne({ code, lang, index, codeShape }).result,
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === "lint") {
|
||||
const indexedCodes = codes.map((code, index) => ({ index, code }));
|
||||
const lintMap = runLintBatch(indexedCodes, lang);
|
||||
return indexedCodes.map(
|
||||
(item) =>
|
||||
lintMap.get(item.index) ??
|
||||
const entries = codes.map((code, index) =>
|
||||
resolveLintEntry({ code, lang, index, codeShape }),
|
||||
);
|
||||
const lintMap = runLintBatch(entries);
|
||||
return entries.map(
|
||||
(entry) =>
|
||||
lintMap.get(entry.index) ??
|
||||
makeResult({
|
||||
isValid: true,
|
||||
errorCount: 0,
|
||||
|
|
@ -344,20 +500,20 @@ function runValidation({ codes, lang, mode }) {
|
|||
);
|
||||
}
|
||||
|
||||
const syntaxResults = codes.map((code, index) =>
|
||||
validateSyntaxOne({ code, lang, index }),
|
||||
const syntaxRuns = codes.map((code, index) =>
|
||||
validateSyntaxOne({ code, lang, index, codeShape }),
|
||||
);
|
||||
const lintTargets = codes
|
||||
.map((code, index) => ({ index, code }))
|
||||
.filter((item) => syntaxResults[item.index]?.is_valid === true);
|
||||
const lintMap = runLintBatch(lintTargets, lang);
|
||||
const lintTargets = syntaxRuns
|
||||
.filter((run) => run.result.is_valid === true)
|
||||
.map((run) => run.lintEntry);
|
||||
const lintMap = runLintBatch(lintTargets);
|
||||
|
||||
return syntaxResults.map((syntaxResult, index) => {
|
||||
if (syntaxResult.is_valid !== true) {
|
||||
return syntaxResult;
|
||||
return syntaxRuns.map((run) => {
|
||||
if (run.result.is_valid !== true) {
|
||||
return run.result;
|
||||
}
|
||||
return (
|
||||
lintMap.get(index) ??
|
||||
lintMap.get(run.lintEntry.index) ??
|
||||
makeResult({
|
||||
isValid: true,
|
||||
errorCount: 0,
|
||||
|
|
@ -389,8 +545,9 @@ async function main() {
|
|||
|
||||
const lang = mapLang(payload?.lang);
|
||||
const mode = mapMode(payload?.mode);
|
||||
const codeShape = mapCodeShape(payload?.code_shape);
|
||||
const codes = Array.isArray(payload?.codes) ? payload.codes : [];
|
||||
const out = runValidation({ codes, lang, mode });
|
||||
const out = runValidation({ codes, lang, mode, codeShape });
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
}
|
||||
|
||||
|
|
@ -398,3 +555,4 @@ main().catch((error) => {
|
|||
process.stderr.write(String(error?.stack || error));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ import {
|
|||
VALIDATOR_OXC_CODE_LANGS,
|
||||
VALIDATOR_SQL_CODE_LANGS,
|
||||
} from "../../utils/validators/code-lang";
|
||||
import {
|
||||
OXC_CODE_SHAPES,
|
||||
normalizeOxcCodeShape,
|
||||
} from "../../utils/validators/oxc-code-shape";
|
||||
import {
|
||||
OXC_VALIDATION_MODES,
|
||||
normalizeOxcValidationMode,
|
||||
|
|
@ -48,10 +52,13 @@ export function ValidatorDialog({
|
|||
const configs = useRecipeStudioStore((state) => state.configs);
|
||||
const targetColumnId = `${config.id}-target-column`;
|
||||
const oxcModeId = `${config.id}-oxc-mode`;
|
||||
const oxcCodeShapeId = `${config.id}-oxc-code-shape`;
|
||||
const batchSizeId = `${config.id}-batch-size`;
|
||||
const oxcModeAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const oxcCodeShapeAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const advancedOpen = config.advancedOpen === true;
|
||||
const selectedOxcMode = normalizeOxcValidationMode(config.oxc_validation_mode);
|
||||
const selectedOxcCodeShape = normalizeOxcCodeShape(config.oxc_code_shape);
|
||||
const codeOptions = useMemo(
|
||||
() =>
|
||||
Object.values(configs)
|
||||
|
|
@ -144,43 +151,84 @@ export function ValidatorDialog({
|
|||
)}
|
||||
</div>
|
||||
{config.validator_type === "oxc" && (
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Validation mode"
|
||||
htmlFor={oxcModeId}
|
||||
hint="syntax: parser only. lint: oxlint only. syntax+lint: both."
|
||||
/>
|
||||
<div ref={oxcModeAnchorRef}>
|
||||
<Combobox
|
||||
items={OXC_VALIDATION_MODES}
|
||||
filteredItems={OXC_VALIDATION_MODES}
|
||||
filter={null}
|
||||
value={selectedOxcMode}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({
|
||||
oxc_validation_mode: normalizeOxcValidationMode(value),
|
||||
})
|
||||
}
|
||||
itemToStringValue={(value) => value}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
id={oxcModeId}
|
||||
className="nodrag w-full"
|
||||
placeholder="Select validation mode"
|
||||
readOnly={true}
|
||||
/>
|
||||
<ComboboxContent anchor={oxcModeAnchorRef}>
|
||||
<ComboboxEmpty>No modes available</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(mode: string) => (
|
||||
<ComboboxItem key={mode} value={mode}>
|
||||
{mode}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Validation mode"
|
||||
htmlFor={oxcModeId}
|
||||
hint="syntax: parser only. lint: oxlint only. syntax+lint: both."
|
||||
/>
|
||||
<div ref={oxcModeAnchorRef}>
|
||||
<Combobox
|
||||
items={OXC_VALIDATION_MODES}
|
||||
filteredItems={OXC_VALIDATION_MODES}
|
||||
filter={null}
|
||||
value={selectedOxcMode}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({
|
||||
oxc_validation_mode: normalizeOxcValidationMode(value),
|
||||
})
|
||||
}
|
||||
itemToStringValue={(value) => value}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
id={oxcModeId}
|
||||
className="nodrag w-full"
|
||||
placeholder="Select validation mode"
|
||||
readOnly={true}
|
||||
/>
|
||||
<ComboboxContent anchor={oxcModeAnchorRef}>
|
||||
<ComboboxEmpty>No modes available</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(mode: string) => (
|
||||
<ComboboxItem key={mode} value={mode}>
|
||||
{mode}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Code shape"
|
||||
htmlFor={oxcCodeShapeId}
|
||||
hint="auto: detect module/snippet. module: strict file. snippet: wrapped fragment."
|
||||
/>
|
||||
<div ref={oxcCodeShapeAnchorRef}>
|
||||
<Combobox
|
||||
items={OXC_CODE_SHAPES}
|
||||
filteredItems={OXC_CODE_SHAPES}
|
||||
filter={null}
|
||||
value={selectedOxcCodeShape}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({
|
||||
oxc_code_shape: normalizeOxcCodeShape(value),
|
||||
})
|
||||
}
|
||||
itemToStringValue={(value) => value}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
id={oxcCodeShapeId}
|
||||
className="nodrag w-full"
|
||||
placeholder="Select code shape"
|
||||
readOnly={true}
|
||||
/>
|
||||
<ComboboxContent anchor={oxcCodeShapeAnchorRef}>
|
||||
<ComboboxEmpty>No code-shape options</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(shape: string) => (
|
||||
<ComboboxItem key={shape} value={shape}>
|
||||
{shape}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export type ValidatorCodeLang =
|
|||
| "sql:ansi";
|
||||
export type ValidatorType = "code" | "oxc";
|
||||
export type OxcValidationMode = "syntax" | "lint" | "syntax+lint";
|
||||
export type OxcCodeShape = "auto" | "module" | "snippet";
|
||||
|
||||
export type ExpressionDtype = "str" | "int" | "float" | "bool";
|
||||
|
||||
|
|
@ -275,6 +276,8 @@ export type ValidatorConfig = {
|
|||
code_lang: ValidatorCodeLang;
|
||||
// ui-only (used for OXC validators)
|
||||
oxc_validation_mode: OxcValidationMode;
|
||||
// ui-only (used for OXC validators)
|
||||
oxc_code_shape: OxcCodeShape;
|
||||
// ui ergonomics (serialized to int in payload)
|
||||
batch_size: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ export function makeValidatorConfig(
|
|||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
code_lang: codeLang,
|
||||
oxc_validation_mode: "syntax",
|
||||
oxc_code_shape: "auto",
|
||||
batch_size: "10",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import type { ValidatorConfig } from "../../../types";
|
||||
import { readNumberString } from "../helpers";
|
||||
import { normalizeValidatorCodeLang } from "../../validators/code-lang";
|
||||
import { normalizeOxcCodeShape } from "../../validators/oxc-code-shape";
|
||||
import { normalizeOxcValidationMode } from "../../validators/oxc-mode";
|
||||
|
||||
const OXC_VALIDATION_FN_MARKER = "unsloth_oxc_validator";
|
||||
|
||||
function parseOxcValidationMarker(
|
||||
validationFunctionRaw: string,
|
||||
): { codeLang: string; mode: string } {
|
||||
): { codeLang: string; mode: string; codeShape: string } {
|
||||
const marker = `${OXC_VALIDATION_FN_MARKER}:`;
|
||||
if (!validationFunctionRaw.startsWith(marker)) {
|
||||
return { codeLang: "", mode: "syntax" };
|
||||
return { codeLang: "", mode: "syntax", codeShape: "auto" };
|
||||
}
|
||||
const parts = validationFunctionRaw
|
||||
.slice(marker.length)
|
||||
|
|
@ -18,11 +19,12 @@ function parseOxcValidationMarker(
|
|||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
return { codeLang: "", mode: "syntax" };
|
||||
return { codeLang: "", mode: "syntax", codeShape: "auto" };
|
||||
}
|
||||
return {
|
||||
codeLang: parts[0],
|
||||
mode: parts[1],
|
||||
codeShape: parts[2] ?? "auto",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ export function parseValidator(
|
|||
validationFunctionRaw.startsWith(OXC_VALIDATION_FN_MARKER);
|
||||
const marker = isOxc
|
||||
? parseOxcValidationMarker(validationFunctionRaw)
|
||||
: { codeLang: "", mode: "syntax" };
|
||||
: { codeLang: "", mode: "syntax", codeShape: "auto" };
|
||||
return {
|
||||
id,
|
||||
kind: "validator",
|
||||
|
|
@ -66,6 +68,9 @@ export function parseValidator(
|
|||
oxc_validation_mode: isOxc
|
||||
? normalizeOxcValidationMode(marker.mode)
|
||||
: "syntax",
|
||||
oxc_code_shape: isOxc
|
||||
? normalizeOxcCodeShape(marker.codeShape)
|
||||
: "auto",
|
||||
batch_size: readNumberString(column.batch_size) || "10",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function buildValidatorColumn(
|
|||
validator_params: {
|
||||
// backend resolves this marker to a real callable.
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
validation_function: `${OXC_VALIDATION_FN_MARKER}:${codeLang}:${config.oxc_validation_mode}`,
|
||||
validation_function: `${OXC_VALIDATION_FN_MARKER}:${codeLang}:${config.oxc_validation_mode}:${config.oxc_code_shape ?? "auto"}`,
|
||||
},
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
batch_size: parseBatchSize(config.batch_size),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
ValidatorConfig,
|
||||
} from "../../types";
|
||||
import { VALIDATOR_OXC_CODE_LANGS } from "../validators/code-lang";
|
||||
import { isOxcCodeShape } from "../validators/oxc-code-shape";
|
||||
import { isOxcValidationMode } from "../validators/oxc-mode";
|
||||
|
||||
export function validateSubcategoryConfigs(
|
||||
|
|
@ -157,6 +158,15 @@ export function validateValidatorConfigs(
|
|||
);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
config.validator_type === "oxc" &&
|
||||
!isOxcCodeShape(config.oxc_code_shape)
|
||||
) {
|
||||
errors.push(
|
||||
`Validator ${config.name}: oxc_code_shape '${config.oxc_code_shape}' is invalid.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
config.validator_type !== "oxc" &&
|
||||
(targetConfig.code_lang ?? "").trim() !== config.code_lang.trim()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { NodeConfig } from "../types";
|
||||
import { isValidSex, parseAgeRange, parseIntNumber, parseNumber } from "./parse";
|
||||
import { VALIDATOR_OXC_CODE_LANGS, VALIDATOR_SQL_CODE_LANGS } from "./validators/code-lang";
|
||||
import { isOxcCodeShape } from "./validators/oxc-code-shape";
|
||||
import { isOxcValidationMode } from "./validators/oxc-mode";
|
||||
|
||||
const TRACE_MODES = new Set(["none", "last_message", "all_messages"]);
|
||||
|
|
@ -214,6 +215,9 @@ export function getConfigErrors(config: NodeConfig | null): string[] {
|
|||
if (!isOxcValidationMode(config.oxc_validation_mode)) {
|
||||
errors.push("OXC validation mode must be syntax, lint, or syntax+lint.");
|
||||
}
|
||||
if (!isOxcCodeShape(config.oxc_code_shape)) {
|
||||
errors.push("OXC code shape must be auto, module, or snippet.");
|
||||
}
|
||||
} else if (
|
||||
config.code_lang !== "python" &&
|
||||
!VALIDATOR_SQL_CODE_LANGS.includes(config.code_lang)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import type { OxcCodeShape } from "../../types";
|
||||
|
||||
export const OXC_CODE_SHAPES: OxcCodeShape[] = [
|
||||
"auto",
|
||||
"module",
|
||||
"snippet",
|
||||
];
|
||||
|
||||
export function isOxcCodeShape(value: string): value is OxcCodeShape {
|
||||
return OXC_CODE_SHAPES.includes(value as OxcCodeShape);
|
||||
}
|
||||
|
||||
export function normalizeOxcCodeShape(value: unknown): OxcCodeShape {
|
||||
if (typeof value !== "string") {
|
||||
return "auto";
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (isOxcCodeShape(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue