export function schemaLinter()

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