in experimenter/experimenter/nimbus-ui/src/components/PageEditBranches/FormBranches/FormFeatureValue/validators.ts [69:213]
export function schemaLinter(schema: Record<string, unknown>) {
const draft = detectDraft(schema);
const EMPTY_STRING_RE = /^\s*$/;
const SCHEMA_RE = /^Property ".*" does not match schema\.$/;
const ADDITIONAL_PROPERTIES_RE =
/^Property "(.*)" does not match additional properties schema.$/;
// Errors that are ignored because they are always accompanied by a more
// specific, useful error message.
const IGNORED_ERRORS = [
"A subschema had errors.",
`Instance does not match "then" schema.`,
`Instance does not match "else" schema.`,
"Instance does not match every subschema.",
"Items did not match schema.",
];
// An error message for when additionalProperties is false and either the
// property does not exist or its value has the wrong type.
const FALSE_BOOLEAN_SCHEMA = "False boolean schema.";
const ADDITIONAL_PROPERTIES = "additionalProperties";
return function (view: TestableEditorView): Diagnostic[] {
const text = view.state.doc.toString();
if (EMPTY_STRING_RE.test(text)) {
return [];
}
let rootNode;
try {
rootNode = jsonToAst(text);
} catch (e: unknown) {
const err = parseError.parse(e);
const pos = documentPosition(view.state.doc, err.line, err.column - 1);
return [
{
from: pos,
to: pos,
message: err.rawMessage,
severity: "error",
},
];
}
// Feature configuration values must be objects.
if (rootNode.type !== "Object") {
let type: string = rootNode.type;
if (rootNode.type === "Literal") {
if (rootNode.value === null) {
type = "null";
} else {
type = typeof rootNode.value;
}
}
return [
{
from: 0,
to: view.state.doc.length,
message: `Expected a JSON Object, not ${type}`,
severity: "error",
},
];
}
// We know JSON.parse(text) will succeed because we already parsed text into
// an ast above.
const validationResult = validate(
JSON.parse(text),
schema,
draft,
undefined,
false,
);
const diagnostics: Diagnostic[] = [];
// Keep track of type errors reported by instanceLocation, because a more
// specific type error will be followed by less helpful, more generic
// errors.
const typeErrors = new Set();
for (const error of validationResult.errors) {
let message = error.error;
if (SCHEMA_RE.exec(message)) {
// We will get a more specific error later.
continue;
}
const instancePath = error.instanceLocation.split("/").slice(1);
let instanceLocation = error.instanceLocation;
let nodeKey: keyof FindNodeResult = "value";
if (error.keyword === "type" && instancePath.length) {
typeErrors.add(error.instanceLocation);
} else if (error.keyword === ADDITIONAL_PROPERTIES) {
const match = ADDITIONAL_PROPERTIES_RE.exec(message);
if (match) {
const propertyName = match[1];
// This error gets reported on the object, not the property. Suppress
// this error if we've already reported a type error for the property.
instanceLocation = `${error.instanceLocation}/${propertyName}`;
if (typeErrors.has(instanceLocation)) {
continue;
}
typeErrors.add(instanceLocation);
instancePath.push(propertyName);
message = `Unexpected property "${propertyName}"`;
nodeKey = "key";
}
} else if (
IGNORED_ERRORS.includes(message) ||
(message === FALSE_BOOLEAN_SCHEMA &&
typeErrors.has(error.instanceLocation))
) {
continue;
}
const pos = findNode(rootNode, instancePath)[nodeKey]!.loc!;
const from = documentPosition(
view.state.doc,
pos.start.line,
pos.start.column - 1,
);
const to = documentPosition(
view.state.doc,
pos.end.line,
pos.end.column - 1,
);
diagnostics.push(createDiagnostic(from, to, message));
}
reportFloatValues(view.state.doc, rootNode, diagnostics);
return diagnostics;
};
}