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/test/CqlAntlr.test.ts b/test/CqlAntlr.test.ts index bc826a6..1dad528 100644 --- a/test/CqlAntlr.test.ts +++ b/test/CqlAntlr.test.ts @@ -21,7 +21,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 +30,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 +45,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 +54,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 +98,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'"); }); 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..5cc3e50 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'