diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 3714f85..44d3bea 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -10,15 +10,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: diff --git a/.github/workflows/unit_test_coverage.yml b/.github/workflows/unit_test_coverage.yml index 636d273..bb1031c 100644 --- a/.github/workflows/unit_test_coverage.yml +++ b/.github/workflows/unit_test_coverage.yml @@ -21,15 +21,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -56,7 +56,7 @@ jobs: run: npm run-script coverage - name: Store the coverage report as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage path: coverage/lcov.info @@ -67,7 +67,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download coverage artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: coverage @@ -83,14 +83,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download coverage artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: coverage - name: Upload code coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: lcov.info fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} # required diff --git a/package-lock.json b/package-lock.json index 61e5b34..c254642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@madie/cql-antlr-parser", - "version": "1.0.7", + "version": "1.0.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@madie/cql-antlr-parser", - "version": "1.0.7", + "version": "1.0.9", "license": "CREATIVE COMMONS ATTRIBUTION 4.0 INTERNATIONAL LICENSE", "dependencies": { "antlr4ts": "^0.5.0-alpha.4", diff --git a/package.json b/package.json index bc4d644..c89290e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@madie/cql-antlr-parser", - "version": "1.0.7", + "version": "1.0.9", "description": "Antlr Parsing of CQL in typescript", "publishConfig": { "access": "public" diff --git a/src/AntlrUtils.ts b/src/AntlrUtils.ts index b6371be..5af3252 100644 --- a/src/AntlrUtils.ts +++ b/src/AntlrUtils.ts @@ -18,29 +18,12 @@ export default class AntlrUtils { } static findChildName(children: ParseTree[] | undefined): string | undefined { - - if (children?.length != 4) { - console.error( - "########### Entering.. children length is ", - children?.length - ); - throw new Error("Definition might be malformed."); - - } return children ? children[1].text : undefined; } static findChildExpression( children: ParseTree[] | undefined ): string | undefined { - - if (children?.length != 4) { - console.error( - "########### Entering.. children length is ", - children?.length - ) - throw new Error("Definition might be malformed."); - } return children ? children[3].text : undefined; } @@ -85,11 +68,12 @@ export default class AntlrUtils { * @param comment -> a comment with comment characters */ static formatComment(comment: string): string { - return comment.replace(AntlrUtils.SINGLE_LINE_COMMENT_REGEX, "") + return comment + .replace(AntlrUtils.SINGLE_LINE_COMMENT_REGEX, "") .replace(AntlrUtils.MULTI_LINE_COMMENT_REGEX, "") .split("\n") .map((line) => line.trim()) - .filter(line=> line !== "") + .filter((line) => line !== "") .join("\n"); } diff --git a/src/CqlAntlr.ts b/src/CqlAntlr.ts index 4f550a7..a8f5991 100644 --- a/src/CqlAntlr.ts +++ b/src/CqlAntlr.ts @@ -1,4 +1,4 @@ -import {BufferedTokenStream, CharStreams, CommonTokenStream} from "antlr4ts"; +import { BufferedTokenStream, CharStreams, CommonTokenStream } from "antlr4ts"; import { CodePointCharStream } from "antlr4ts/CodePointCharStream"; import { ParseTreeWalker } from "antlr4ts/tree"; import { cqlLexer, cqlParser, LibraryContext, cqlListener } from "../generated"; @@ -18,9 +18,12 @@ class CqlAntlr { const charStream: CodePointCharStream = CharStreams.fromString(this.cql); const lexer: cqlLexer = new cqlLexer(charStream); const bufferedTokenStream = new BufferedTokenStream(lexer); - bufferedTokenStream.fill() + bufferedTokenStream.fill(); - const listener: cqlListener = new CqlAntlrListener(result, bufferedTokenStream); + const listener: cqlListener = new CqlAntlrListener( + result, + bufferedTokenStream + ); ParseTreeWalker.DEFAULT.walk(listener, tree); /** * Disabled. Only partially implemented and would be diff --git a/src/CqlAntlrListener.ts b/src/CqlAntlrListener.ts index ed53b7a..9acc649 100644 --- a/src/CqlAntlrListener.ts +++ b/src/CqlAntlrListener.ts @@ -6,6 +6,7 @@ import { cqlLexer, cqlListener, ExpressionDefinitionContext, + FunctionDefinitionContext, IncludeDefinitionContext, LibraryDefinitionContext, ParameterDefinitionContext, @@ -27,13 +28,14 @@ import CqlParameter from "./dto/CqlParameter"; import CqlContextCreator from "./CqlContextCreator"; import CqlExpressionDefinition from "./dto/CqlExpressionDefinition"; import CqlExpressionDefinitionCreator from "./CqlExpressionDefinitionCreator"; -import {CqlCode} from "./dto"; +import { CqlCode } from "./dto"; import CqlIdentifier from "./dto/CqlIdentifier"; import CqlIdentifierCreator from "./CqlIdentifierCreator"; import CqlRetrieve from "./dto/CqlRetrieve"; import CqlRetrieveCreator from "./CqlRetrieveCreator"; -import {BufferedTokenStream} from "antlr4ts"; +import { BufferedTokenStream } from "antlr4ts"; import AntlrUtils from "./AntlrUtils"; +import { ParserRuleContext } from "antlr4ts/ParserRuleContext"; export default class CqlAntlrListener implements cqlListener { // save bufferedTokenStream from lexer @@ -107,23 +109,29 @@ export default class CqlAntlrListener implements cqlListener { this.cqlResult.context = new CqlContextCreator(ctx).buildDao(); } - enterExpressionDefinition(ctx: ExpressionDefinitionContext): void { - const cqlExpressionCreator = new CqlExpressionDefinitionCreator(ctx); - const expressionDefinition: CqlExpressionDefinition | undefined = cqlExpressionCreator.buildDao(); - - if (expressionDefinition) { - if (ctx.start.inputStream) { - const hiddenTokens = this.bufferedTokenStream.getHiddenTokensToLeft(ctx.start.tokenIndex, cqlLexer.HIDDEN) - let comment = ""; - hiddenTokens.forEach((token) => { - if (token.text){ - comment += token.text; - } - }) - comment = comment.trim(); - if (comment){ - // if expression has comment, start needs to be adjusted to consider comments as comment is part of definition - expressionDefinition.start = cqlExpressionCreator.buildLineInfo(hiddenTokens[1]); + private processDefinitionWithComments( + ctx: ParserRuleContext, + buildDao: () => CqlExpressionDefinition | undefined + ): void { + const expressionDefinition: CqlExpressionDefinition | undefined = + buildDao(); + + if (expressionDefinition && ctx.start.inputStream) { + const hiddenTokens = this.bufferedTokenStream.getHiddenTokensToLeft( + ctx.start.tokenIndex, + cqlLexer.HIDDEN + ); + + if (hiddenTokens && hiddenTokens.length > 0) { + const comment = hiddenTokens + .map((token) => token.text?.trim()) + .filter(Boolean) + .join(" "); + + if (comment) { + expressionDefinition.start = new CqlExpressionDefinitionCreator( + ctx + ).buildLineInfo(hiddenTokens[1]); expressionDefinition.comment = AntlrUtils.formatComment(comment); } } @@ -131,6 +139,18 @@ export default class CqlAntlrListener implements cqlListener { } } + enterExpressionDefinition(ctx: ExpressionDefinitionContext): void { + this.processDefinitionWithComments(ctx, () => { + return new CqlExpressionDefinitionCreator(ctx).buildDao(); + }); + } + + enterFunctionDefinition(ctx: FunctionDefinitionContext): void { + this.processDefinitionWithComments(ctx, () => { + return new CqlExpressionDefinitionCreator(ctx).buildDao(); + }); + } + enterAggregateClause(ctx: AggregateClauseContext): void { const identifier: CqlIdentifier | undefined = new CqlIdentifierCreator( ctx diff --git a/src/CustomErrorListener.ts b/src/CustomErrorListener.ts index b3d0925..f9b4fbb 100644 --- a/src/CustomErrorListener.ts +++ b/src/CustomErrorListener.ts @@ -3,6 +3,7 @@ import { Recognizer } from "antlr4ts/Recognizer"; import { Token } from "antlr4ts/Token"; import { ParserATNSimulator } from "antlr4ts/atn/ParserATNSimulator"; import CqlResult from "./dto/CqlResult"; +import convertCustomError from "./util/CustomeErrorConverter"; /** * Fires on grammar errors. @@ -32,7 +33,7 @@ export default class CustomErrorListener implements ANTLRErrorListener { 1, // plus 1 to ensure full text is included in Ace Editor highlight }, name: offendingSymbol.text, - message: msg, + message: convertCustomError(msg), }); } } diff --git a/src/util/CustomeErrorConverter.ts b/src/util/CustomeErrorConverter.ts new file mode 100644 index 0000000..c449556 --- /dev/null +++ b/src/util/CustomeErrorConverter.ts @@ -0,0 +1,15 @@ +const convertCustomError = (errorMessage: string): string => { + let convertedMsg: string = errorMessage; + switch (errorMessage) { + case "no viable alternative at input 'define :'": { + convertedMsg = "Definition is missing a name."; + break; + } + default: { + break; + } + } + return convertedMsg; +}; + +export default convertCustomError; diff --git a/test/CqlAntlr.test.ts b/test/CqlAntlr.test.ts index bc826a6..498c25c 100644 --- a/test/CqlAntlr.test.ts +++ b/test/CqlAntlr.test.ts @@ -10,6 +10,7 @@ import { cqlFluentFunctions, relatedContextRetrieve, aggregateQuery, + cqlDefineWithNoName, } from "./testCql"; import { CqlAntlr } from "../src"; import CqlResult from "../src/dto/CqlResult"; @@ -21,7 +22,7 @@ describe("test antlr", () => { expect(cqlResult.valueSets.length).toBe(0); expect(cqlResult.codeSystems.length).toBe(0); expect(cqlResult.parameters.length).toBe(0); - expect(cqlResult.expressionDefinitions.length).toEqual(0) + expect(cqlResult.expressionDefinitions.length).toEqual(0); }); it("parse blank CQL", () => { @@ -30,7 +31,7 @@ describe("test antlr", () => { expect(cqlResult.valueSets.length).toBe(0); expect(cqlResult.codeSystems.length).toBe(0); expect(cqlResult.parameters.length).toBe(0); - expect(cqlResult.expressionDefinitions.length).toEqual(0) + expect(cqlResult.expressionDefinitions.length).toEqual(0); }); it("parse simple Fhir CQL Definition", () => { @@ -45,7 +46,7 @@ describe("test antlr", () => { expect(cqlResult.parameters.length).toBe(0); - expect(cqlResult.expressionDefinitions.length).toEqual(4); + expect(cqlResult.expressionDefinitions.length).toEqual(6); cqlResult.expressionDefinitions.forEach((def) => { expect(def.name).toBeDefined(); }); @@ -54,11 +55,21 @@ describe("test antlr", () => { const cqlAntlr = new CqlAntlr(simpleDefinitionCql); const cqlResult: CqlResult = cqlAntlr.parse(); const expressions = cqlResult.expressionDefinitions; - expect(expressions.length).toEqual(4); - expect(cqlResult.expressionDefinitions[0].comment).toEqual("ehnicity comment"); + expect(expressions.length).toEqual(6); + expect(cqlResult.expressionDefinitions[0].comment).toEqual( + "ehnicity comment" + ); expect(cqlResult.expressionDefinitions[1].comment).toEqual("multi line"); - expect(cqlResult.expressionDefinitions[2].comment).toEqual("@author: john doe\n@description: this is Numerator"); + expect(cqlResult.expressionDefinitions[2].comment).toEqual( + "@author: john doe\n@description: this is Numerator" + ); expect(cqlResult.expressionDefinitions[3].comment).toEqual(undefined); + expect(cqlResult.expressionDefinitions[4].comment).toEqual( + "multiline comment outside of a function with multiple\nrows" + ); + expect(cqlResult.expressionDefinitions[5].comment).toEqual( + "comment outside of function" + ); }); it("parse fhir cql", () => { const cqlAntlr = new CqlAntlr(fhirTestCql); @@ -88,11 +99,11 @@ describe("test antlr", () => { expect(cqlResult.usings[0]?.name).toBe("QDM"); expect(cqlResult.valueSets.length).toBe(2); expect(cqlResult.valueSets[0].name).toBe( - "\"Adolescent depression screening assessment\"" + '"Adolescent depression screening assessment"' ); expect(cqlResult.valueSets[0].version).toBeUndefined(); expect(cqlResult.valueSets[1].name).toBe( - "\"Adolescent depression screening assessment with version\"" + '"Adolescent depression screening assessment with version"' ); expect(cqlResult.valueSets[1].version).toBe("'urn:hl7:version:20240307'"); }); @@ -167,4 +178,13 @@ describe("test antlr", () => { const cqlResult: CqlResult = cqlAntlr.parse(); expect(cqlResult.errors.length).toEqual(0); }); + + it("test define with no name", (): void => { + const cqlAntlr = new CqlAntlr(cqlDefineWithNoName); + const cqlResult: CqlResult = cqlAntlr.parse(); + expect(cqlResult.errors.length).toEqual(1); + expect(cqlResult.errors[0].message).toEqual( + "Definition is missing a name." + ); + }); }); diff --git a/test/CqlExpressionVisitor.test.ts b/test/CqlExpressionVisitor.test.ts index 4979c97..dd9fa51 100644 --- a/test/CqlExpressionVisitor.test.ts +++ b/test/CqlExpressionVisitor.test.ts @@ -57,9 +57,9 @@ describe("test visitor", () => { cqlResult.includes.push(createInclude("FHIRHelpers")); cqlResult.includes.push(createInclude("Global")); - cqlResult.valueSets.push(createValueSet("\"Patient Refusal\"")); - cqlResult.valueSets.push(createValueSet("\"Medical Reason\"")); - cqlResult.valueSets.push(createValueSet("\"Antithrombotic Therapy\"")); + cqlResult.valueSets.push(createValueSet('"Patient Refusal"')); + cqlResult.valueSets.push(createValueSet('"Medical Reason"')); + cqlResult.valueSets.push(createValueSet('"Antithrombotic Therapy"')); const cqlExpressionVisitor = new CqlExpressionVisitor(cqlResult); cqlExpressionVisitor.visit(createAntlrContext(testDefineWithAlias)); @@ -101,9 +101,9 @@ describe("test visitor", () => { cqlResult.includes.push(createInclude("FHIRHelpers")); cqlResult.includes.push(createInclude("Global")); - cqlResult.valueSets.push(createValueSet("\"Patient Refusal\"")); - cqlResult.valueSets.push(createValueSet("\"Medical Reason\"")); - cqlResult.valueSets.push(createValueSet("\"Antithrombotic Therapy\"")); + cqlResult.valueSets.push(createValueSet('"Patient Refusal"')); + cqlResult.valueSets.push(createValueSet('"Medical Reason"')); + cqlResult.valueSets.push(createValueSet('"Antithrombotic Therapy"')); const v = new CqlExpressionVisitor(cqlResult); v.visit(createAntlrContext(sdeValueset)); diff --git a/test/testCql.ts b/test/testCql.ts index a920f58..4755168 100644 --- a/test/testCql.ts +++ b/test/testCql.ts @@ -25,6 +25,19 @@ const simpleDefinitionCql = ` ( ["MedicationAdministration": medication in "Low Dose Unfractionated Heparin for VTE Prophylaxis"] VTEMedication where VTEMedication.status = 'completed' ) + + /* +multiline comment outside of a function with multiple +rows +*/ + +define function "Denominator Observation"(Encounter "Encounter, Performed" ): + // inside of definition to ignore + duration in hours of Encounter.relevantPeriod + +// comment outside of function +define function "Numerator Observation"(Encounter "Encounter, Performed" ): + duration in hours of Encounter.relevantPeriod `; const fhirTestCql = `library TJCOverall_FHIR4 version '4.0.000' @@ -207,6 +220,10 @@ define FactorialOfFive: aggregate Result starting 1: Result * Num `; +const cqlDefineWithNoName = ` +define : + true +`; export { simpleDefinitionCql, fhirTestCql, @@ -219,4 +236,5 @@ export { cqlFluentFunctions, relatedContextRetrieve, aggregateQuery, + cqlDefineWithNoName, };