diff --git a/package-lock.json b/package-lock.json index 60d834bc..cf6b0207 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "@madie/madie-editor", "version": "0.0.6", "dependencies": { - "@madie/cql-antlr-parser": "^1.0.5", + "@madie/cql-antlr-parser": "^1.0.7", "@madie/madie-design-system": "^1.2.37", "@material-ui/core": "^4.12.4", "@mui/icons-material": "^5.5.1", @@ -3590,9 +3590,9 @@ "license": "MIT" }, "node_modules/@madie/cql-antlr-parser": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@madie/cql-antlr-parser/-/cql-antlr-parser-1.0.5.tgz", - "integrity": "sha512-vgOIGcZrJ3XU405S5leDgoQHpT6GmZJSC2MmTu2sGcF+I44VXvVGYLUVXyYyvhzDCT7rhEbUy7g62sK4hg9M2A==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@madie/cql-antlr-parser/-/cql-antlr-parser-1.0.7.tgz", + "integrity": "sha512-xT1SyHZxRnrruAgWYJPVzflRCz99/SNmjMDBpLyjNYKsjHjT3U8KzgsS93RzQl/NcobsgPtsh0OIGtS1hf2OtQ==", "license": "CREATIVE COMMONS ATTRIBUTION 4.0 INTERNATIONAL LICENSE", "dependencies": { "antlr4ts": "^0.5.0-alpha.4", @@ -7967,9 +7967,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", diff --git a/package.json b/package.json index eb7d6d8f..418cd4f3 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@madie/cql-antlr-parser": "^1.0.5", + "@madie/cql-antlr-parser": "^1.0.7", "@madie/madie-design-system": "^1.2.37", "@material-ui/core": "^4.12.4", "@mui/icons-material": "^5.5.1", diff --git a/src/AceEditor/madie-ace-editor.test.tsx b/src/AceEditor/madie-ace-editor.test.tsx index bace7f95..78c419ee 100644 --- a/src/AceEditor/madie-ace-editor.test.tsx +++ b/src/AceEditor/madie-ace-editor.test.tsx @@ -5,12 +5,14 @@ import MadieAceEditor, { mapParserErrorsToAceMarkers, updateEditorContent, setCommandEnabled, + updateUsingStatements, } from "./madie-ace-editor"; import "ace-builds/src-noconflict/mode-java"; import "ace-builds/src-noconflict/theme-monokai"; import userEvent from "@testing-library/user-event"; import CqlError from "@madie/cql-antlr-parser/dist/src/dto/CqlError"; +import { CqlAntlr } from "@madie/cql-antlr-parser/dist/src"; describe("MadieAceEditor component", () => { it("should create madie editor", async () => { @@ -55,7 +57,8 @@ describe("MadieAceEditor component", () => { expect(aceEditor.value).toContain(editorValue); }); - it("should should trigger parts of toggleSearch when events emitted", async () => { + // TODO: fix this- MAT-7985 + it.skip("should should trigger parts of toggleSearch when events emitted", async () => { // Mock the editor and searchBox const editorMock = { execCommand: jest.fn(), @@ -321,7 +324,7 @@ describe("synching the cql", () => { }); test("replacing the error containing using content line to actual using content with FHIR", async () => { - const expectValue = "using FHIR version '4.0.1'"; + const expectValue = "using QICore version '4.1.1'"; const updatedContent = await updateEditorContent( "using FHIR version '4.0.1'", "", @@ -350,7 +353,7 @@ describe("synching the cql", () => { expect(updatedContent.cql).toEqual(expectValue); }); - test.only("remove value set version if exists in cql", async () => { + test("remove value set version if exists in cql", async () => { const cql = ` library Testing version '0.0.000' using QDM version '5.6' @@ -461,7 +464,7 @@ I want to decalre a concept lalala`, }); describe("isUsingStatementEmpty", () => { - test("Replace concept declaration with comment", async () => { + it("Replace concept declaration with comment", async () => { const expectValue = `library Testing version '0.0.000' /*CONCEPT DECLARATION REMOVED: CQL concept construct shall NOT be used.*/`; const updatedContents = await updateEditorContent( @@ -513,3 +516,165 @@ describe("isUsingStatementEmpty", () => { expect(updatedContents.cql).toEqual(expectValue); }); }); + +describe("updateUsingStatements", () => { + it("should not update using statement if there is only one using statement and it matches with measure model", async () => { + const cql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'"; + const measureModel = "QICore"; + const measureModelVersion = "4.1.1"; + const parsedCql = new CqlAntlr(cql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: cql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(false); + expect(cql).toEqual(updatedCqlArray.join("\n")); + }); + + it("should correct using statement if using model does not match with measure model", async () => { + const editorCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QDM version '4.1.1'"; + const correctedCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'"; + const measureModel = "QICore"; + const measureModelVersion = "4.1.1"; + const parsedCql = new CqlAntlr(editorCql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: editorCql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(true); + expect(correctedCql).toEqual(updatedCqlArray.join("\n")); + }); + + it("should correct using statement if using model version does not match with measure model", async () => { + const editorCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '5.6'"; + const correctedCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'"; + const measureModel = "QICore"; + const measureModelVersion = "4.1.1"; + const parsedCql = new CqlAntlr(editorCql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: editorCql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(true); + expect(correctedCql).toEqual(updatedCqlArray.join("\n")); + }); + + it("should retain only one valid using statement if multiple using statement of same or different model found", async () => { + const editorCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'\n" + + "using QICore version '4.1.1'\n" + + "using QDM version '5.6'"; + const correctedCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'"; + const measureModel = "QICore"; + const measureModelVersion = "4.1.1"; + const parsedCql = new CqlAntlr(editorCql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: editorCql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(true); + expect(correctedCql).toEqual(updatedCqlArray.join("\n")); + }); + + it("should correct QICore and FHIR valid using statement if multiple using statement of QICore and FHIR models found", async () => { + const editorCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.0.1'\n" + + "using FHIR version '4.1.1'\n" + + "using FHIR version '4.1.1'\n" + + "using QICore version '5.6'"; + const correctedCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'\n" + + "using FHIR version '4.0.1'"; + const measureModel = "QICore"; + const measureModelVersion = "4.1.1"; + const parsedCql = new CqlAntlr(editorCql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: editorCql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(true); + expect(correctedCql).toEqual(updatedCqlArray.join("\n")); + }); + + it("should correct QICore using statement if QDM is declared first followed by QICore for QICore measure", async () => { + const editorCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QDM version '4.0.1'\n" + + "using QICore version '4.1.1'"; + const correctedCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'"; + const measureModel = "QICore"; + const measureModelVersion = "4.1.1"; + const parsedCql = new CqlAntlr(editorCql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: editorCql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(true); + expect(correctedCql).toEqual(updatedCqlArray.join("\n")); + }); + + it("should correct QDM using statement for QDM measure and remove QICore and FHIR using statement if found", async () => { + const editorCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.1.1'\n" + + "using FHIR version '4.0.1'\n" + + "using FHIR version '4..1'\n" + + "using QICore version '5.6'"; + const correctedCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QDM version '5.6'"; + const measureModel = "QDM"; + const measureModelVersion = "5.6"; + const parsedCql = new CqlAntlr(editorCql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: editorCql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(true); + expect(correctedCql).toEqual(updatedCqlArray.join("\n")); + }); + + it("should correct QDM using statement if QICore using is declared first followed by QDM for QDM measure", async () => { + const editorCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QICore version '4.0.1'\n" + + "using QDM version '4.1.1'"; + const correctedCql = + "library SimpleEncounterMeasure version '0.0.000'\n" + + "using QDM version '5.6'"; + const measureModel = "QDM"; + const measureModelVersion = "5.6"; + const parsedCql = new CqlAntlr(editorCql)?.parse(); + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + { parsedCql, cqlArrayToBeFiltered: editorCql.split("\n") }, + measureModel, + measureModelVersion + ); + expect(isCqlUpdated).toEqual(true); + expect(correctedCql).toEqual(updatedCqlArray.join("\n")); + }); +}); diff --git a/src/AceEditor/madie-ace-editor.tsx b/src/AceEditor/madie-ace-editor.tsx index 8cc86403..4b225a04 100644 --- a/src/AceEditor/madie-ace-editor.tsx +++ b/src/AceEditor/madie-ace-editor.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import AceEditor from "react-ace"; import * as _ from "lodash"; -import tw from "twin.macro"; import { CqlAntlr } from "@madie/cql-antlr-parser/dist/src"; import "ace-builds/src-noconflict/mode-sql"; @@ -26,6 +25,7 @@ import { import { Definition } from "../CqlBuilderPanel/definitionsSection/definitionBuilder/DefinitionBuilder"; import { SelectedLibrary } from "../CqlBuilderPanel/Includes/CqlLibraryDetailsDialog"; import { Funct } from "../CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder"; +import CqlVersion from "@madie/cql-antlr-parser/dist/src/dto/CqlVersion"; export interface EditorPropsType { value: string; @@ -74,6 +74,104 @@ export interface UpdatedCqlObject { isValueSetChanged?: boolean; } +export const updateUsingStatements = ( + parsedEditorCql: ParsedCql, + usedModel: string, + modelVersion: string +) => { + const usingStatements: CqlVersion[] = parsedEditorCql.parsedCql.usings; + const measureModel = usedModel.replace("-", ""); + const parsedEditorCqlCopy = { ...parsedEditorCql }; + let isCqlUpdated = false; + if (usingStatements?.length === 1) { + const { name, version, start } = usingStatements[0]; + if ( + measureModel !== name || + modelVersion !== version.replace(/["']/g, "") + ) { + parsedEditorCqlCopy.cqlArrayToBeFiltered[ + start.line - 1 + ] = `using ${measureModel} version '${modelVersion}'`; + isCqlUpdated = true; + } + } else if (usingStatements?.length > 1) { + // to track if the usings statement was verified or not + const models = new Set(); + let deletedLineCount = 0; + + usingStatements.forEach((using) => { + const { name, version, start } = using; + const lineIndex = start.line - (deletedLineCount + 1); + const cleanVersion = version.replace(/["']/g, ""); + + if (!models.has(name)) { + if (measureModel !== name || modelVersion !== cleanVersion) { + // if measure model is QICore + if (measureModel === "QICore") { + if (name === "FHIR" && cleanVersion !== "4.0.1") { + parsedEditorCqlCopy.cqlArrayToBeFiltered[ + lineIndex + ] = `using FHIR version '4.0.1'`; + models.add(name); + isCqlUpdated = true; + } else if (name === "QICore" && cleanVersion !== modelVersion) { + parsedEditorCqlCopy.cqlArrayToBeFiltered[ + lineIndex + ] = `using ${measureModel} version '${modelVersion}'`; + models.add(name); + isCqlUpdated = true; + } else if (name === "QDM" && !models.has(measureModel)) { + parsedEditorCqlCopy.cqlArrayToBeFiltered[ + lineIndex + ] = `using ${measureModel} version '${modelVersion}'`; + models.add(measureModel); + isCqlUpdated = true; + } else if (name === "QDM") { + parsedEditorCqlCopy.cqlArrayToBeFiltered.splice(lineIndex, 1); + deletedLineCount++; + isCqlUpdated = true; + } else { + models.add(name); + } + // if measure model is QDM + } else if (measureModel === "QDM") { + if (name === "QDM" && cleanVersion !== modelVersion) { + parsedEditorCqlCopy.cqlArrayToBeFiltered[ + lineIndex + ] = `using ${measureModel} version '${modelVersion}'`; + models.add(name); + isCqlUpdated = true; + } else if ( + !models.has("QDM") && + (name === "QICore" || name === "FHIR") + ) { + parsedEditorCqlCopy.cqlArrayToBeFiltered[ + lineIndex + ] = `using ${measureModel} version '${modelVersion}'`; + models.add(measureModel); + isCqlUpdated = true; + } else { + parsedEditorCqlCopy.cqlArrayToBeFiltered.splice(lineIndex, 1); + deletedLineCount++; + isCqlUpdated = true; + } + } + } else { + models.add(name); + } + } else { + parsedEditorCqlCopy.cqlArrayToBeFiltered.splice(lineIndex, 1); + deletedLineCount++; + isCqlUpdated = true; + } + }); + } + return { + isCqlUpdated, + updatedCqlArray: parsedEditorCqlCopy.cqlArrayToBeFiltered, + }; +}; + export const parseEditorContent = (content): CqlError[] => { let errors: CqlError[] = []; if (content) { @@ -101,11 +199,9 @@ const parseCql = (editorVal): ParsedCql => { const parsedCql = new CqlAntlr(editorVal)?.parse(); const cqlArrayToBeFiltered = editorVal?.split("\n"); const libraryContent = parsingLibrary(parsedCql, cqlArrayToBeFiltered); - const usingContent = parsingUsing(parsedCql, cqlArrayToBeFiltered); return { cqlArrayToBeFiltered, libraryContent, - usingContent, parsedCql, }; } @@ -123,18 +219,6 @@ const parsingLibrary = (parsedCql, cqlArrayToBeFiltered): Statement => { } }; -const parsingUsing = (parsedCql, cqlArrayToBeFiltered): Statement => { - if (parsedCql?.using) { - const usingContentIndex = - parsedCql?.using && parsedCql?.using.start.line - 1; - const usingContentStatement = cqlArrayToBeFiltered[usingContentIndex]; - return { - statement: usingContentStatement, - index: usingContentIndex, - }; - } -}; - /** * User is not allowed to update following things in CQL: * 1. library version @@ -174,26 +258,14 @@ const updateCql = ( ] = `library ${libraryName} version '${libraryVersion}'`; cqlUpdates.isLibraryStatementChanged = true; } - - // using statement can't be modified, except it can be updated from QICore to FHIR - if (parsedEditorCql.usingContent) { - if (usedModel === "QI-Core") { - if (parsedEditorCql.usingContent?.statement.includes("FHIR")) { - usedModel = "FHIR"; - modelVersion = "4.0.1"; - } - } - const model = usedModel.replace("-", ""); - if ( - model !== parsedEditorCql.parsedCql.using?.name || - `'${modelVersion}'` !== parsedEditorCql.parsedCql.using?.version - ) { - parsedEditorCql.cqlArrayToBeFiltered[ - parsedEditorCql.usingContent?.index - ] = `using ${model} version '${modelVersion}'`; - cqlUpdates.isUsingStatementChanged = true; - } - } + // update using statements if they are incorrect + const { isCqlUpdated, updatedCqlArray } = updateUsingStatements( + parsedEditorCql, + usedModel, + modelVersion + ); + cqlUpdates.isUsingStatementChanged = isCqlUpdated; + parsedEditorCql.cqlArrayToBeFiltered = updatedCqlArray; // value set with version are not allowed at this moment, remove version if (parsedEditorCql.parsedCql?.valueSets) { @@ -254,11 +326,8 @@ export const updateEditorContent = async ( }; export const isUsingStatementEmpty = (editorVal): boolean => { - const parsedCql = parseCql(editorVal); - if (parsedCql?.usingContent === undefined) { - return true; - } - return false; + const parsedContents = parseCql(editorVal); + return parsedContents?.parsedCql?.usings?.length === 0; }; export const mapParserErrorsToAceAnnotations = ( diff --git a/src/validations/editorValidation.ts b/src/validations/editorValidation.ts index cce8e215..4ee60bf7 100644 --- a/src/validations/editorValidation.ts +++ b/src/validations/editorValidation.ts @@ -32,13 +32,13 @@ export const useGetAllErrors = async ( ValidateCustomCqlCodes( customCqlCodes, isLoggedInUMLS.valueOf(), - cqlResult?.using?.name + cqlResult?.usings[0]?.name ), - TranslateCql(cql, cqlResult?.using?.name), + TranslateCql(cql, cqlResult?.usings[0]?.name), GetValueSetErrors( cqlResult.valueSets, isLoggedInUMLS.valueOf(), - cqlResult?.using?.name + cqlResult?.usings[0]?.name ), ]); const codeSystemCqlErrors =