From e8fccf0ff38c11d804a20d6416c2a90609fe84a4 Mon Sep 17 00:00:00 2001 From: JordanBoltonMN Date: Tue, 26 Aug 2025 13:25:26 -0500 Subject: [PATCH 1/2] initial commit --- package-lock.json | 4 +- package.json | 2 +- .../language/identifierExpressionUtils.ts | 25 ++++++++ .../language/identifierUtils.ts | 27 +++++++++ src/powerquery-parser/language/index.ts | 3 +- .../identifierExpressionUtils.test.ts | 57 +++++++++++++++++++ src/test/libraryTest/identifierUtils.test.ts | 7 +++ 7 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/powerquery-parser/language/identifierExpressionUtils.ts create mode 100644 src/test/libraryTest/identifierExpressionUtils.test.ts diff --git a/package-lock.json b/package-lock.json index 94c5ebb2..7cf76b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/powerquery-parser", - "version": "0.18.0", + "version": "0.18.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@microsoft/powerquery-parser", - "version": "0.18.0", + "version": "0.18.1", "license": "MIT", "dependencies": { "grapheme-splitter": "^1.0.4", diff --git a/package.json b/package.json index 90deb0a2..ddd37d16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/powerquery-parser", - "version": "0.18.0", + "version": "0.18.1", "description": "A parser for the Power Query/M formula language.", "author": "Microsoft", "license": "MIT", diff --git a/src/powerquery-parser/language/identifierExpressionUtils.ts b/src/powerquery-parser/language/identifierExpressionUtils.ts new file mode 100644 index 00000000..3d91415b --- /dev/null +++ b/src/powerquery-parser/language/identifierExpressionUtils.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { CommonIdentifierUtilsOptions, getNormalizedIdentifier } from "./identifierUtils"; +import { Assert } from "../common"; + +export function assertNormalizedIdentifierExpression(text: string, options?: CommonIdentifierUtilsOptions): string { + return Assert.asDefined( + getNormalizedIdentifierExpression(text, options), + `Expected a valid identifier expression but received '${text}'`, + ); +} + +// Removes the '@' and quotes from a quoted identifier if possible. +// When given an invalid identifier, returns undefined. +export function getNormalizedIdentifierExpression( + text: string, + options?: CommonIdentifierUtilsOptions, +): string | undefined { + if (text.startsWith("@")) { + text = text.substring(1); + } + + return getNormalizedIdentifier(text, options); +} diff --git a/src/powerquery-parser/language/identifierUtils.ts b/src/powerquery-parser/language/identifierUtils.ts index 995983e2..f17d94ca 100644 --- a/src/powerquery-parser/language/identifierUtils.ts +++ b/src/powerquery-parser/language/identifierUtils.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { Assert, Pattern, StringUtils } from "../common"; +import { KeywordKind } from "./keyword/keyword"; export enum IdentifierKind { Generalized = "Generalized", @@ -21,6 +22,14 @@ export interface GetAllowedIdentifiersOptions extends CommonIdentifierUtilsOptio readonly allowRecursive?: boolean; } +// Wraps an assert around the getNormalizedIdentifier method +export function assertNormalizedIdentifier(text: string, options?: CommonIdentifierUtilsOptions): string { + return Assert.asDefined( + getNormalizedIdentifier(text, options), + `Expected a valid identifier but received '${text}'`, + ); +} + // Identifiers have multiple forms that can be used interchangeably. // For example, if you have `[key = 1]`, you can use `key` or `#""key""`. // The `getAllowedIdentifiers` function returns all the forms of the identifier that are allowed in the current context. @@ -180,6 +189,10 @@ export function getIdentifierLength( // Removes the quotes from a quoted identifier if possible. // When given an invalid identifier, returns undefined. export function getNormalizedIdentifier(text: string, options?: CommonIdentifierUtilsOptions): string | undefined { + if (AllowedHashKeywords.has(text)) { + return text; + } + const allowGeneralizedIdentifier: boolean = options?.allowGeneralizedIdentifier ?? DefaultAllowGeneralizedIdentifier; @@ -367,3 +380,17 @@ function stripQuotes(text: string): string { const DefaultAllowTrailingPeriod: boolean = false; const DefaultAllowGeneralizedIdentifier: boolean = false; + +const AllowedHashKeywords: ReadonlySet = new Set([ + KeywordKind.HashBinary, + KeywordKind.HashDate, + KeywordKind.HashDateTime, + KeywordKind.HashDateTimeZone, + KeywordKind.HashDuration, + KeywordKind.HashInfinity, + KeywordKind.HashNan, + KeywordKind.HashSections, + KeywordKind.HashShared, + KeywordKind.HashTable, + KeywordKind.HashTime, +]); diff --git a/src/powerquery-parser/language/index.ts b/src/powerquery-parser/language/index.ts index ccf37cba..cd92006a 100644 --- a/src/powerquery-parser/language/index.ts +++ b/src/powerquery-parser/language/index.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as Comment from "./comment"; +export * as IdentifierExpressionUtils from "./identifierExpressionUtils"; export * as IdentifierUtils from "./identifierUtils"; export * as TextUtils from "./textUtils"; +import * as Comment from "./comment"; import * as Token from "./token"; export { Comment, Token }; diff --git a/src/test/libraryTest/identifierExpressionUtils.test.ts b/src/test/libraryTest/identifierExpressionUtils.test.ts new file mode 100644 index 00000000..296bb6ac --- /dev/null +++ b/src/test/libraryTest/identifierExpressionUtils.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "mocha"; +import { expect } from "chai"; + +import { IdentifierExpressionUtils, IdentifierUtils } from "../../powerquery-parser/language"; + +describe("IdentifierUtils", () => { + function createCommonIdentifierUtilsOptions( + overrides?: Partial, + ): IdentifierUtils.CommonIdentifierUtilsOptions { + return { + allowTrailingPeriod: false, + allowGeneralizedIdentifier: false, + ...overrides, + }; + } + + describe(`getNormalizedIdentifierExpression`, () => { + function runGetNormalizedIdentifierExpressionTest(params: { + readonly text: string; + readonly expectedSuccess: string | undefined; + readonly options?: Partial; + }): void { + const text: string = params.text; + + const identifierUtilsOptions: IdentifierUtils.CommonIdentifierUtilsOptions = + createCommonIdentifierUtilsOptions(params.options); + + const actual: string | undefined = IdentifierExpressionUtils.getNormalizedIdentifierExpression( + text, + identifierUtilsOptions, + ); + + if (params.expectedSuccess !== undefined) { + expect(actual).to.equal(params.expectedSuccess); + } else { + expect(actual).to.be.undefined; + } + } + + it("foo", () => { + runGetNormalizedIdentifierExpressionTest({ + text: "foo", + expectedSuccess: "foo", + }); + }); + + it("@foo", () => { + runGetNormalizedIdentifierExpressionTest({ + text: "@foo", + expectedSuccess: "foo", + }); + }); + }); +}); diff --git a/src/test/libraryTest/identifierUtils.test.ts b/src/test/libraryTest/identifierUtils.test.ts index 9d30f000..aba22fc5 100644 --- a/src/test/libraryTest/identifierUtils.test.ts +++ b/src/test/libraryTest/identifierUtils.test.ts @@ -368,5 +368,12 @@ describe("IdentifierUtils", () => { expectedSuccess: "quoted generalized identifier", }); }); + + it("#table", () => { + runGetNormalizedIdentifierTest({ + text: "#table", + expectedSuccess: "#table", + }); + }); }); }); From 4becfcb55a175e0d8f1f709b8428fb20c8b914f0 Mon Sep 17 00:00:00 2001 From: JordanBoltonMN Date: Tue, 26 Aug 2025 13:28:43 -0500 Subject: [PATCH 2/2] small additional test --- src/test/libraryTest/identifierExpressionUtils.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/libraryTest/identifierExpressionUtils.test.ts b/src/test/libraryTest/identifierExpressionUtils.test.ts index 296bb6ac..22ba6b8f 100644 --- a/src/test/libraryTest/identifierExpressionUtils.test.ts +++ b/src/test/libraryTest/identifierExpressionUtils.test.ts @@ -53,5 +53,12 @@ describe("IdentifierUtils", () => { expectedSuccess: "foo", }); }); + + it("#table", () => { + runGetNormalizedIdentifierExpressionTest({ + text: "#table", + expectedSuccess: "#table", + }); + }); }); });