Fixes #9477 feat(nimbus): Add linter for FML (#9521)

Because

- We need a linter for the FML so that we can fetch errors for feature
configs

This commit

- First pass at an FML linter that will fetch errors from the FML and
parse them into `Diagnostic`s
- We do not have the endpoint yet that is going to fetch the errors
(#9480)
- Separates out some shared helpers for both schema linter and fml
linter
- Rename `schema.ts` to `validators.ts` so we can include the fml
linting in the same file. This can be separated out later if need be
This commit is contained in:
Elise Richards 2023-10-06 15:52:33 -07:00 коммит произвёл GitHub
Родитель c42cbe90d7
Коммит e019771d55
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 95 добавлений и 16 удалений

Просмотреть файл

@ -11,9 +11,12 @@ import React, { useCallback, useMemo, useRef } from "react";
import { useController, useFormContext } from "react-hook-form";
import { FeatureValueEditorProps } from "src/components/PageEditBranches/FormBranches/FormFeatureValue/props";
import {
fmlLinter,
schemaAutocomplete,
schemaLinter,
} from "src/components/PageEditBranches/FormBranches/FormFeatureValue/schema";
} from "src/components/PageEditBranches/FormBranches/FormFeatureValue/validators";
const allowFmlLinting = false;
export default function RichFeatureValueEditor({
featureConfig,
@ -81,8 +84,11 @@ export default function RichFeatureValueEditor({
];
if (schema) {
extensions.push(linter(schemaLinter(schema)));
if (allowFmlLinting) {
extensions.push(linter(fmlLinter()));
} else {
extensions.push(linter(schemaLinter(schema)));
}
const completionSource = schemaAutocomplete(schema);
if (completionSource) {
extensions.push(

Просмотреть файл

@ -3,10 +3,11 @@ import { EditorState, EditorStateConfig } from "@codemirror/state";
import { basicSetup } from "codemirror";
import {
detectDraft,
fmlLinter,
schemaAutocomplete,
schemaLinter,
simpleObjectSchema,
} from "src/components/PageEditBranches/FormBranches/FormFeatureValue/schema";
} from "src/components/PageEditBranches/FormBranches/FormFeatureValue/validators";
import { z } from "zod";
const SIMPLE_SCHEMA: z.infer<typeof simpleObjectSchema> = {
@ -329,3 +330,37 @@ describe("detectDraft", () => {
},
);
});
describe("fmlLinter", () => {
it.each(["", " ", "\t", "\n", " \n \t "])(
"does not return fml errors for an empty document",
(doc) => {
const linter = fmlLinter();
const state = createEditorState({ doc: JSON.stringify(doc) });
const diagnostics = linter({ state });
expect(diagnostics).toEqual([]);
},
);
it.each([`{"foo": {"error"}`, `{"error": {"bingo"}`])(
"returns FML errors",
(doc) => {
const linter = fmlLinter();
const message = { message: "oh no!" };
const state = createEditorState({ doc: JSON.stringify(doc) });
const diagnostics = linter({ state });
expect(diagnostics).toContainEqual(expect.objectContaining(message));
},
);
it.each([`{"foo": {"bar"}`, `{"foo": {"mimsy"}`])(
"does not returns FML errors",
(doc) => {
const linter = fmlLinter();
const message = { message: "oh no!" };
const state = createEditorState({ doc: JSON.stringify(doc) });
const diagnostics = linter({ state });
expect(diagnostics).not.toContainEqual(expect.objectContaining(message));
},
);
});

Просмотреть файл

@ -6,6 +6,7 @@ import { Text } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import jsonToAst from "json-to-ast";
import { z } from "zod";
/**
* A simplified schema that is generated by a feature manifest entry with only a
* simple list of variables.
@ -201,12 +202,7 @@ export function schemaLinter(schema: Record<string, unknown>) {
pos.end.column - 1,
);
diagnostics.push({
from,
to,
message,
severity: "error",
});
diagnostics.push(createDiagnostic(from, to, message));
}
reportFloatValues(view.state.doc, rootNode, diagnostics);
@ -222,6 +218,23 @@ function documentPosition(doc: Text, line: number, column: number): number {
return doc.line(line).from + column;
}
/**
* Parse an error into a Diagnostic
*/
function createDiagnostic(
from: number,
to: number,
message: string,
severity = "error",
): Diagnostic {
return {
from: from,
to: to,
message: message,
severity: severity,
} as Diagnostic;
}
interface FindNodeResult {
value: jsonToAst.ValueNode;
key?: jsonToAst.IdentifierNode;
@ -280,12 +293,13 @@ function reportFloatValues(
case "Literal":
if (typeof node.value === "number" && ~~node.value !== node.value) {
const loc = node.loc!;
diagnostics.push({
from: documentPosition(doc, loc.start.line, loc.start.column),
to: documentPosition(doc, loc.start.line, loc.start.column),
severity: "error",
message: "Floats are not supported",
});
diagnostics.push(
createDiagnostic(
documentPosition(doc, loc.start.line, loc.start.column),
documentPosition(doc, loc.start.line, loc.start.column),
"Floats are not supported",
),
);
}
break;
}
@ -332,3 +346,27 @@ export function schemaAutocomplete(schema: Record<string, unknown>) {
return null;
};
}
/**
* Parse from FML errors to Diagnostics
*/
function parseFmlErrors(view: TestableEditorView): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
const doc = view.state.doc;
const text = doc.toString();
const from = text.indexOf("error");
if (from === -1) return diagnostics;
const to = from + "error".length;
diagnostics.push(createDiagnostic(from, to, "oh no!"));
return diagnostics;
}
export function fmlLinter() {
return function (view: TestableEditorView) {
// TODO call endpoint to retrieve errors, remove temp error highlighting
return parseFmlErrors(view);
};
}