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..22ba6b8f --- /dev/null +++ b/src/test/libraryTest/identifierExpressionUtils.test.ts @@ -0,0 +1,64 @@ +// 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", + }); + }); + + it("#table", () => { + runGetNormalizedIdentifierExpressionTest({ + text: "#table", + expectedSuccess: "#table", + }); + }); + }); +}); 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", + }); + }); }); });