feat(recipe-studio, validators): extend OXC validator with code shape support and integrate into recipe studio

This commit is contained in:
Shine1i 2026-03-06 02:04:05 +01:00
commit 93063c3212
10 changed files with 379 additions and 116 deletions

View file

@ -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:

View file

@ -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);
});

View file

@ -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>
)}

View file

@ -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;
};

View file

@ -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",
};
}

View file

@ -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",
};
}

View file

@ -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),

View file

@ -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()

View file

@ -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)

View file

@ -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";
}