diff --git a/package-lock.json b/package-lock.json index b6adbe93..94c5ebb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/powerquery-parser", - "version": "0.17.0", + "version": "0.18.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@microsoft/powerquery-parser", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "dependencies": { "grapheme-splitter": "^1.0.4", diff --git a/package.json b/package.json index 0933673b..90deb0a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/powerquery-parser", - "version": "0.17.0", + "version": "0.18.0", "description": "A parser for the Power Query/M formula language.", "author": "Microsoft", "license": "MIT", diff --git a/src/powerquery-parser/language/identifierUtils.ts b/src/powerquery-parser/language/identifierUtils.ts index 92154e01..995983e2 100644 --- a/src/powerquery-parser/language/identifierUtils.ts +++ b/src/powerquery-parser/language/identifierUtils.ts @@ -5,39 +5,114 @@ import { Assert, Pattern, StringUtils } from "../common"; export enum IdentifierKind { Generalized = "Generalized", + GeneralizedWithQuotes = "GeneralizedWithQuotes", Invalid = "Invalid", - Quote = "Quote", - QuoteRequired = "QuoteRequired", Regular = "Regular", + RegularWithQuotes = "RegularWithQuotes", + RegularWithRequiredQuotes = "RegularWithRequiredQuotes", } -// Assuming the text is a quoted identifier, finds the quotes that enclose the identifier. -// Otherwise returns undefined. -export function findQuotedIdentifierQuotes(text: string, index: number): StringUtils.FoundQuotes | undefined { - if (text[index] !== "#") { - return undefined; +export interface CommonIdentifierUtilsOptions { + readonly allowGeneralizedIdentifier?: boolean; + readonly allowTrailingPeriod?: boolean; +} + +export interface GetAllowedIdentifiersOptions extends CommonIdentifierUtilsOptions { + readonly allowRecursive?: boolean; +} + +// 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. +export function getAllowedIdentifiers(text: string, options?: GetAllowedIdentifiersOptions): ReadonlyArray { + const allowGeneralizedIdentifier: boolean = + options?.allowGeneralizedIdentifier ?? DefaultAllowGeneralizedIdentifier; + + const quotedAndUnquoted: TQuotedAndUnquoted | undefined = getQuotedAndUnquoted(text, options); + + if (quotedAndUnquoted === undefined) { + return []; } - return StringUtils.findQuotes(text, index + 1); + let result: string[]; + + switch (quotedAndUnquoted.identifierKind) { + case IdentifierKind.Generalized: + case IdentifierKind.GeneralizedWithQuotes: + result = allowGeneralizedIdentifier ? [quotedAndUnquoted.withQuotes, quotedAndUnquoted.withoutQuotes] : []; + break; + + case IdentifierKind.Invalid: + result = []; + break; + + case IdentifierKind.RegularWithQuotes: + result = [quotedAndUnquoted.withQuotes, quotedAndUnquoted.withoutQuotes]; + break; + + case IdentifierKind.RegularWithRequiredQuotes: + result = [quotedAndUnquoted.withQuotes]; + break; + + case IdentifierKind.Regular: + result = [quotedAndUnquoted.withoutQuotes, quotedAndUnquoted.withQuotes]; + break; + + default: + throw Assert.isNever(quotedAndUnquoted); + } + + if (options?.allowRecursive) { + result = result.concat(result.map((value: string) => prefixInclusiveConstant(value))); + } + + return result; } -// Determines what kind of identifier the text is. -// It's possible that the text is a partially completed identifier, -// which is why we have the `allowTrailingPeriod` parameter. -export function getIdentifierKind(text: string, allowTrailingPeriod: boolean): IdentifierKind { - if (isRegularIdentifier(text, allowTrailingPeriod)) { +// An identifier can have multiple forms: +// - Regular: `foo` +// - Regular with quotes: `#""foo""` +// - Regular with required quotes: `#""foo bar""` +// - Regular with required quotes is used when the identifier has spaces or special characters, +// and when generalized identifiers are not allowed. +// - Generalized: `foo bar` +// - Generalized with quotes: `#""foo bar""` +// - Invalid: `foo..bar` +export function getIdentifierKind(text: string, options?: CommonIdentifierUtilsOptions): IdentifierKind { + const allowGeneralizedIdentifier: boolean = + options?.allowGeneralizedIdentifier ?? DefaultAllowGeneralizedIdentifier; + + if (isRegularIdentifier(text, options)) { return IdentifierKind.Regular; - } else if (isQuotedIdentifier(text)) { - return isRegularIdentifier(text.slice(2, -1), false) ? IdentifierKind.Quote : IdentifierKind.QuoteRequired; - } else if (isGeneralizedIdentifier(text)) { + } else if (allowGeneralizedIdentifier && isGeneralizedIdentifier(text)) { return IdentifierKind.Generalized; + } + // If the identifier is quoted it's either: + // - a regular identifier with quotes, + // - a generalized identifier with quotes, + else if (isQuotedIdentifier(text)) { + const stripped: string = stripQuotes(text); + + if (isRegularIdentifier(stripped, options)) { + return IdentifierKind.RegularWithQuotes; + } else if (isGeneralizedIdentifier(stripped) && allowGeneralizedIdentifier) { + return IdentifierKind.GeneralizedWithQuotes; + } else { + return IdentifierKind.RegularWithRequiredQuotes; + } } else { return IdentifierKind.Invalid; } } -// Assuming the text is an identifier, returns the length of the identifier. -export function getIdentifierLength(text: string, index: number, allowTrailingPeriod: boolean): number | undefined { +// I'd prefer if this was internal, but it's used by the lexer so it's marked as public. +// Returns the length of the identifier starting at the given index. +export function getIdentifierLength( + text: string, + index: number, + options?: CommonIdentifierUtilsOptions, +): number | undefined { + const allowTrailingPeriod: boolean = options?.allowTrailingPeriod ?? DefaultAllowTrailingPeriod; const startingIndex: number = index; const textLength: number = text.length; @@ -62,26 +137,37 @@ export function getIdentifierLength(text: string, index: number, allowTrailingPe break; - case IdentifierRegexpState.RegularIdentifier: - // Don't consider `..` or `...` part of an identifier. - if (allowTrailingPeriod && text[index] === "." && text[index + 1] !== ".") { - index += 1; - } + // We should allow a single period as part of the identifier, + // but only if it's not the last character and not followed by another period. + // Allow an exception for when it's the last character and allowTrailingPeriod is true. + case IdentifierRegexpState.RegularIdentifier: { + const currentChr: string | undefined = text[index]; - matchLength = StringUtils.regexMatchLength(Pattern.IdentifierPartCharacters, text, index); - - if (matchLength === undefined) { + if (currentChr === undefined) { state = IdentifierRegexpState.Done; - } else { - index += matchLength; + } else if (currentChr === ".") { + const nextChr: string | undefined = text[index + 1]; - // Don't consider `..` or `...` part of an identifier. - if (allowTrailingPeriod && text[index] === "." && text[index + 1] !== ".") { + // If we have a single period we might include it as part of the identifier when: + // 1. It's not the last character and not followed by another period + // 2. It's the last character and allowTrailingPeriod is true + if ((nextChr && nextChr !== ".") || (nextChr === undefined && allowTrailingPeriod)) { index += 1; + } else { + state = IdentifierRegexpState.Done; + } + } else { + matchLength = StringUtils.regexMatchLength(Pattern.IdentifierPartCharacters, text, index); + + if (matchLength === undefined) { + state = IdentifierRegexpState.Done; + } else { + index += matchLength; } } break; + } default: throw Assert.isNever(state); @@ -91,8 +177,81 @@ export function getIdentifierLength(text: string, index: number, allowTrailingPe return index !== startingIndex ? index - startingIndex : undefined; } +// 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 { + const allowGeneralizedIdentifier: boolean = + options?.allowGeneralizedIdentifier ?? DefaultAllowGeneralizedIdentifier; + + const quotedAndUnquoted: TQuotedAndUnquoted = getQuotedAndUnquoted(text, options); + + switch (quotedAndUnquoted.identifierKind) { + case IdentifierKind.Regular: + case IdentifierKind.RegularWithQuotes: + return quotedAndUnquoted.withoutQuotes; + + case IdentifierKind.GeneralizedWithQuotes: + case IdentifierKind.Generalized: + return allowGeneralizedIdentifier ? quotedAndUnquoted.withoutQuotes : undefined; + + case IdentifierKind.Invalid: + return undefined; + + case IdentifierKind.RegularWithRequiredQuotes: + return quotedAndUnquoted.withQuotes; + + default: + throw Assert.isNever(quotedAndUnquoted); + } +} + +type TQuotedAndUnquoted = + | { + readonly identifierKind: IdentifierKind.Generalized; + readonly withQuotes: string; + readonly withoutQuotes: string; + } + | { + readonly identifierKind: IdentifierKind.GeneralizedWithQuotes; + readonly withQuotes: string; + readonly withoutQuotes: string; + } + | { + readonly identifierKind: IdentifierKind.Invalid; + } + | { + readonly identifierKind: IdentifierKind.Regular; + readonly withQuotes: string; + readonly withoutQuotes: string; + } + | { + readonly identifierKind: IdentifierKind.RegularWithQuotes; + readonly withQuotes: string; + readonly withoutQuotes: string; + } + | { + readonly identifierKind: IdentifierKind.RegularWithRequiredQuotes; + readonly withQuotes: string; + }; + +const enum IdentifierRegexpState { + Done = "Done", + RegularIdentifier = "RegularIdentifier", + Start = "Start", +} + +// Finds the locations of quotes in a quoted identifier. +// Returns undefined if the identifier is not quoted. +function findQuotedIdentifierQuotes(text: string, index: number): StringUtils.FoundQuotes | undefined { + if (text[index] !== "#") { + return undefined; + } + + return StringUtils.findQuotes(text, index + 1); +} + // Assuming the text is a generalized identifier, returns the length of the identifier. -export function getGeneralizedIdentifierLength(text: string, index: number): number | undefined { +function getGeneralizedIdentifierLength(text: string, index: number): number | undefined { const startingIndex: number = index; const textLength: number = text.length; @@ -133,31 +292,78 @@ export function getGeneralizedIdentifierLength(text: string, index: number): num return index !== startingIndex ? index - startingIndex : undefined; } -export function isGeneralizedIdentifier(text: string): boolean { - return getGeneralizedIdentifierLength(text, 0) === text.length; +// Returns the quoted and unquoted versions of the identifier (if applicable). +function getQuotedAndUnquoted(text: string, options?: CommonIdentifierUtilsOptions): TQuotedAndUnquoted { + const identifierKind: IdentifierKind = getIdentifierKind(text, options); + + switch (identifierKind) { + case IdentifierKind.Generalized: + return { + identifierKind, + withoutQuotes: text, + withQuotes: makeQuoted(text), + }; + + case IdentifierKind.GeneralizedWithQuotes: + return { + identifierKind, + withoutQuotes: stripQuotes(text), + withQuotes: text, + }; + + case IdentifierKind.Invalid: + return { + identifierKind, + }; + + case IdentifierKind.RegularWithQuotes: + return { + identifierKind, + withoutQuotes: stripQuotes(text), + withQuotes: text, + }; + + case IdentifierKind.RegularWithRequiredQuotes: + return { + identifierKind, + withQuotes: text, + }; + + case IdentifierKind.Regular: + return { + identifierKind, + withoutQuotes: text, + withQuotes: makeQuoted(text), + }; + + default: + throw Assert.isNever(identifierKind); + } } -export function isRegularIdentifier(text: string, allowTrailingPeriod: boolean): boolean { - return getIdentifierLength(text, 0, allowTrailingPeriod) === text.length; +function makeQuoted(text: string): string { + return `#"${text}"`; } -export function isQuotedIdentifier(text: string): boolean { - return findQuotedIdentifierQuotes(text, 0) !== undefined; +function prefixInclusiveConstant(text: string): string { + return `@${text}`; } -// Removes the quotes from a quoted identifier if possible. -export function normalizeIdentifier(text: string): string { - if (isQuotedIdentifier(text)) { - const stripped: string = text.slice(2, -1); +function isGeneralizedIdentifier(text: string): boolean { + return text.length > 0 && getGeneralizedIdentifierLength(text, 0) === text.length; +} - return isRegularIdentifier(stripped, false) ? stripped : text; - } else { - return text; - } +function isRegularIdentifier(text: string, options?: CommonIdentifierUtilsOptions): boolean { + return text.length > 0 && getIdentifierLength(text, 0, options) === text.length; } -const enum IdentifierRegexpState { - Done = "Done", - RegularIdentifier = "RegularIdentifier", - Start = "Start", +function isQuotedIdentifier(text: string): boolean { + return findQuotedIdentifierQuotes(text, 0) !== undefined; +} + +function stripQuotes(text: string): string { + return text.slice(2, -1); } + +const DefaultAllowTrailingPeriod: boolean = false; +const DefaultAllowGeneralizedIdentifier: boolean = false; diff --git a/src/powerquery-parser/language/type/typeUtils/isEqualType.ts b/src/powerquery-parser/language/type/typeUtils/isEqualType.ts index 9b02e9bf..4a097487 100644 --- a/src/powerquery-parser/language/type/typeUtils/isEqualType.ts +++ b/src/powerquery-parser/language/type/typeUtils/isEqualType.ts @@ -22,7 +22,12 @@ export function isEqualType(left: Type.TPowerQueryType, right: Type.TPowerQueryT } export function isEqualFunctionParameter(left: Type.FunctionParameter, right: Type.FunctionParameter): boolean { - return left.isNullable !== right.isNullable || left.isOptional !== right.isOptional || left.type !== right.type; + return ( + left.nameLiteral === right.nameLiteral && + left.isNullable === right.isNullable && + left.isOptional === right.isOptional && + left.type === right.type + ); } export function isEqualFunctionSignature( diff --git a/src/powerquery-parser/lexer/lexer.ts b/src/powerquery-parser/lexer/lexer.ts index 494db56d..958e9f92 100644 --- a/src/powerquery-parser/lexer/lexer.ts +++ b/src/powerquery-parser/lexer/lexer.ts @@ -1015,7 +1015,6 @@ function readKeyword(text: string, lineNumber: number, positionStart: number, lo function readKeywordHelper(text: string, currentPosition: number): Token.LineToken | undefined { const identifierPositionStart: number = text[currentPosition] === "#" ? currentPosition + 1 : currentPosition; - const identifierPositionEnd: number | undefined = indexOfIdentifierEnd(text, identifierPositionStart); if (identifierPositionEnd === undefined) { @@ -1137,7 +1136,9 @@ function indexOfRegexEnd(pattern: RegExp, text: string, positionStart: number): } function indexOfIdentifierEnd(text: string, positionStart: number): number | undefined { - const length: number | undefined = IdentifierUtils.getIdentifierLength(text, positionStart, true); + const length: number | undefined = IdentifierUtils.getIdentifierLength(text, positionStart, { + allowTrailingPeriod: true, + }); return length !== undefined ? positionStart + length : undefined; } diff --git a/src/powerquery-parser/parser/nodeIdMap/nodeIdMapIterator.ts b/src/powerquery-parser/parser/nodeIdMap/nodeIdMapIterator.ts index 7f4d565b..4d485c37 100644 --- a/src/powerquery-parser/parser/nodeIdMap/nodeIdMapIterator.ts +++ b/src/powerquery-parser/parser/nodeIdMap/nodeIdMapIterator.ts @@ -342,7 +342,10 @@ export function iterFieldSpecificationList( keyLiteral, optional, value, - normalizedKeyLiteral: IdentifierUtils.normalizeIdentifier(keyLiteral), + normalizedKeyLiteral: Assert.asDefined( + IdentifierUtils.getNormalizedIdentifier(keyLiteral, { allowGeneralizedIdentifier: true }), + `Expected key "${keyLiteral}" to be a valid identifier.`, + ), pairKind: PairKind.FieldSpecification, source: fieldSpecification, }); @@ -380,6 +383,7 @@ export function iterLetExpression( nodeIdMapCollection, arrayWrapper, PairKind.LetExpression, + { allowGeneralizedIdentifier: false }, ); } @@ -410,6 +414,7 @@ export function iterRecord( nodeIdMapCollection, arrayWrapper, PairKind.Record, + { allowGeneralizedIdentifier: true }, ); } @@ -450,7 +455,10 @@ export function iterSection( source: XorNodeUtils.boxAst(namePairedExpression), key: namePairedExpression.key, keyLiteral, - normalizedKeyLiteral: IdentifierUtils.normalizeIdentifier(keyLiteral), + normalizedKeyLiteral: Assert.asDefined( + IdentifierUtils.getNormalizedIdentifier(keyLiteral, { allowGeneralizedIdentifier: true }), + `Expected key "${keyLiteral}" to be a valid identifier.`, + ), value: XorNodeUtils.boxAst(namePairedExpression.value), pairKind: PairKind.SectionMember, }; @@ -504,7 +512,10 @@ export function iterSection( source: keyValuePair, key, keyLiteral, - normalizedKeyLiteral: IdentifierUtils.normalizeIdentifier(keyLiteral), + normalizedKeyLiteral: Assert.asDefined( + IdentifierUtils.getNormalizedIdentifier(keyLiteral, { allowGeneralizedIdentifier: true }), + `Expected key "${keyLiteral}" to be a valid identifier.`, + ), value: NodeIdMapUtils.nthChildXor(nodeIdMapCollection, keyValuePairNodeId, 2), pairKind: PairKind.SectionMember, }); @@ -520,6 +531,7 @@ function iterKeyValuePairs< nodeIdMapCollection: NodeIdMap.Collection, arrayWrapper: TXorNode, pairKind: TKeyValuePair["pairKind"], + identifierUtilsOptions: IdentifierUtils.CommonIdentifierUtilsOptions, ): ReadonlyArray { const partial: KVP[] = []; @@ -539,7 +551,10 @@ function iterKeyValuePairs< source: keyValuePair, key, keyLiteral, - normalizedKeyLiteral: IdentifierUtils.normalizeIdentifier(keyLiteral), + normalizedKeyLiteral: Assert.asDefined( + IdentifierUtils.getNormalizedIdentifier(keyLiteral, identifierUtilsOptions), + `Expected key "${keyLiteral}" to be a valid identifier.`, + ), value: NodeIdMapUtils.nthChildXor(nodeIdMapCollection, keyValuePair.node.id, 2), pairKind, } as KVP); diff --git a/src/powerquery-parser/parser/parsers/naiveParseSteps.ts b/src/powerquery-parser/parser/parsers/naiveParseSteps.ts index 1017eb54..1ae59704 100644 --- a/src/powerquery-parser/parser/parsers/naiveParseSteps.ts +++ b/src/powerquery-parser/parser/parsers/naiveParseSteps.ts @@ -121,7 +121,11 @@ export async function readGeneralizedIdentifier( const contiguousIdentifierStartIndex: number = tokens[tokenRangeStartIndex].positionStart.codeUnit; const contiguousIdentifierEndIndex: number = tokens[tokenRangeEndIndex - 1].positionEnd.codeUnit; const literal: string = lexerSnapshot.text.slice(contiguousIdentifierStartIndex, contiguousIdentifierEndIndex); - const literalKind: IdentifierUtils.IdentifierKind = IdentifierUtils.getIdentifierKind(literal, true); + + const literalKind: IdentifierUtils.IdentifierKind = IdentifierUtils.getIdentifierKind(literal, { + allowTrailingPeriod: true, + allowGeneralizedIdentifier: true, + }); if (literalKind === IdentifierUtils.IdentifierKind.Invalid) { trace.exit({ diff --git a/src/test/libraryTest/identifierUtils.test.ts b/src/test/libraryTest/identifierUtils.test.ts index e592b737..9d30f000 100644 --- a/src/test/libraryTest/identifierUtils.test.ts +++ b/src/test/libraryTest/identifierUtils.test.ts @@ -4,54 +4,369 @@ import "mocha"; import { expect } from "chai"; +import { IdentifierKind } from "../../powerquery-parser/language/identifierUtils"; import { IdentifierUtils } from "../../powerquery-parser/language"; describe("IdentifierUtils", () => { - describe(`isRegularIdentifier`, () => { - describe(`valid`, () => { - it(`foo`, () => expect(IdentifierUtils.isRegularIdentifier("foo", false), "should be true").to.be.true); - it(`foo`, () => expect(IdentifierUtils.isRegularIdentifier("foo", true), "should be true").to.be.true); - it(`foo.`, () => expect(IdentifierUtils.isRegularIdentifier("foo.", true), "should be true").to.be.true); - it(`foo.1`, () => expect(IdentifierUtils.isRegularIdentifier("foo.1", true), "should be true").to.be.true); + function createCommonIdentifierUtilsOptions( + overrides?: Partial, + ): IdentifierUtils.CommonIdentifierUtilsOptions { + return { + allowTrailingPeriod: false, + allowGeneralizedIdentifier: false, + ...overrides, + }; + } - it(`foo.bar123`, () => - expect(IdentifierUtils.isRegularIdentifier("foo.bar123", true), "should be true").to.be.true); + function createGetAllowedIdentifiersOptions( + overrides?: Partial, + ): IdentifierUtils.GetAllowedIdentifiersOptions { + return { + allowRecursive: false, + ...overrides, + }; + } + + describe(`getAllowedIdentifiers`, () => { + function getAllowedIdentifiersTest(params: { + readonly text: string; + readonly expected: ReadonlyArray; + readonly options?: Partial; + }): void { + const text: string = params.text; + + const options: IdentifierUtils.GetAllowedIdentifiersOptions = createGetAllowedIdentifiersOptions( + params.options, + ); + + const actual: ReadonlyArray = IdentifierUtils.getAllowedIdentifiers(text, options); + expect(actual).to.have.members(params.expected); + } + + it("foo", () => { + getAllowedIdentifiersTest({ + text: "foo", + expected: ["foo", `#"foo"`], + }); + }); + + it("[empty string]", () => { + getAllowedIdentifiersTest({ + text: "", + expected: [], + }); + }); + + it("foo. // allowTrailingPeriod - true", () => { + getAllowedIdentifiersTest({ + text: "foo.", + options: { allowTrailingPeriod: true }, + expected: ["foo.", `#"foo."`], + }); + }); + + it("foo. // allowTrailingPeriod - false", () => { + getAllowedIdentifiersTest({ + text: "foo.", + options: { allowTrailingPeriod: false }, + expected: [], + }); }); - describe(`invalid`, () => { - it(`foo.`, () => expect(IdentifierUtils.isRegularIdentifier("foo.", false), "should be false").to.be.false); + it("foo.bar", () => { + getAllowedIdentifiersTest({ + text: "foo.bar", + expected: ["foo.bar", `#"foo.bar"`], + }); + }); + + it("foo.1", () => { + getAllowedIdentifiersTest({ + text: "foo.1", + expected: ["foo.1", `#"foo.1"`], + }); + }); + + it("with space // allowGeneralizedIdentifier - false", () => { + getAllowedIdentifiersTest({ + text: "with space", + options: { allowGeneralizedIdentifier: false }, + expected: [], + }); + }); + + it("with space // allowGeneralizedIdentifier - true", () => { + getAllowedIdentifiersTest({ + text: "with space", + options: { allowGeneralizedIdentifier: true }, + expected: ["with space", `#"with space"`], + }); + }); + + it(`#"regularIdentifierWithUnneededQuotes" // allowGeneralizedIdentifier - false`, () => { + getAllowedIdentifiersTest({ + text: '#"regularIdentifierWithUnneededQuotes"', + options: { allowGeneralizedIdentifier: false }, + expected: ["regularIdentifierWithUnneededQuotes", `#"regularIdentifierWithUnneededQuotes"`], + }); + }); + + it(`#"quoted regular identifier" // allowGeneralizedIdentifier - false`, () => { + getAllowedIdentifiersTest({ + text: '#"quoted regular identifier"', + options: { allowGeneralizedIdentifier: false }, + expected: [`#"quoted regular identifier"`], + }); + }); + + it(`#"quoted generalized identifier" // allowGeneralizedIdentifier - true`, () => { + getAllowedIdentifiersTest({ + text: '#"quoted generalized identifier"', + options: { allowGeneralizedIdentifier: true }, + expected: ["quoted generalized identifier", `#"quoted generalized identifier"`], + }); + }); + + it("foo // allowRecursive - true", () => { + getAllowedIdentifiersTest({ + text: "foo", + options: { allowRecursive: true }, + expected: ["foo", `#"foo"`, "@foo", `@#"foo"`], + }); }); }); - describe(`isGeneralizedIdentifier`, () => { - describe(`valid`, () => { - it("a", () => expect(IdentifierUtils.isGeneralizedIdentifier("a"), "should be true").to.be.true); - it("a.1", () => expect(IdentifierUtils.isGeneralizedIdentifier("a.1"), "should be true").to.be.true); - it("a b", () => expect(IdentifierUtils.isGeneralizedIdentifier("a b"), "should be true").to.be.true); + describe(`getIdentifierKind`, () => { + function runGetIdentifierKindTest(params: { + readonly text: string; + readonly expected: IdentifierKind; + readonly options?: Partial; + }): void { + const text: string = params.text; + + const options: IdentifierUtils.CommonIdentifierUtilsOptions = createCommonIdentifierUtilsOptions( + params.options, + ); + + const actual: IdentifierKind = IdentifierUtils.getIdentifierKind(text, options); + expect(actual).to.equal(params.expected); + } + + it("foo", () => { + runGetIdentifierKindTest({ + text: "foo", + expected: IdentifierKind.Regular, + }); + }); + + it("", () => { + runGetIdentifierKindTest({ + text: "", + expected: IdentifierKind.Invalid, + }); + }); + + it("foo. // allowTrailingPeriod - true", () => { + runGetIdentifierKindTest({ + text: "foo.", + options: { allowTrailingPeriod: true }, + expected: IdentifierKind.Regular, + }); + }); + + it("foo. // allowTrailingPeriod - false", () => { + runGetIdentifierKindTest({ + text: "foo.", + options: { allowTrailingPeriod: false }, + expected: IdentifierKind.Invalid, + }); + }); + + it("foo.bar", () => { + runGetIdentifierKindTest({ + text: "foo.bar", + expected: IdentifierKind.Regular, + }); + }); + + it("foo..bar", () => { + runGetIdentifierKindTest({ + text: "foo..bar", + expected: IdentifierKind.Invalid, + }); }); - describe(`invalid`, () => { - it("a..1", () => expect(IdentifierUtils.isGeneralizedIdentifier("a..1"), "should be false").to.be.false); + it("foo.bar.baz.green.eggs.and.ham", () => { + runGetIdentifierKindTest({ + text: "foo.bar.baz.green.eggs.and.ham", + expected: IdentifierKind.Regular, + }); + }); + + it("foo.bar.baz.green.eggs.and.ham. // allowTrailingPeriod - true", () => { + runGetIdentifierKindTest({ + text: "foo.bar.baz.green.eggs.and.ham", + options: { allowTrailingPeriod: true }, + expected: IdentifierKind.Regular, + }); + }); + + it("foo.bar.baz.green.eggs.and.ham. // allowTrailingPeriod - false", () => { + runGetIdentifierKindTest({ + text: "foo.bar.baz.green.eggs.and.ham.", + options: { allowTrailingPeriod: false }, + expected: IdentifierKind.Invalid, + }); + }); + + it("foo.1", () => { + runGetIdentifierKindTest({ + text: "foo.1", + expected: IdentifierKind.Regular, + }); + }); + + it("with space // allowGeneralizedIdentifier - false", () => { + runGetIdentifierKindTest({ + text: "with space", + options: { allowGeneralizedIdentifier: false }, + expected: IdentifierKind.Invalid, + }); + }); + + it("with space // allowGeneralizedIdentifier - true", () => { + runGetIdentifierKindTest({ + text: "with space", + options: { allowGeneralizedIdentifier: true }, + expected: IdentifierKind.Generalized, + }); + }); + + it("with space", () => { + runGetIdentifierKindTest({ + text: '#"quoteNotNeeded"', + expected: IdentifierKind.RegularWithQuotes, + }); + }); + + it(`#"quote needed"`, () => { + runGetIdentifierKindTest({ + text: '#"quote needed"', + expected: IdentifierKind.RegularWithRequiredQuotes, + }); + }); + + it(`#"quoted generalized identifier"' // allowGeneralizedIdentifier - true`, () => { + runGetIdentifierKindTest({ + text: '#"quoted generalized identifier"', + options: { allowGeneralizedIdentifier: true }, + expected: IdentifierKind.GeneralizedWithQuotes, + }); }); }); - describe(`isQuotedIdentifier`, () => { - describe(`valid`, () => { - it(`#"foo"`, () => expect(IdentifierUtils.isQuotedIdentifier(`#"foo"`), "should be true").to.be.true); - it(`#""`, () => expect(IdentifierUtils.isQuotedIdentifier(`#""`), "should be true").to.be.true); - it(`#""""`, () => expect(IdentifierUtils.isQuotedIdentifier(`#""""`), "should be true").to.be.true); + describe(`getNormalizedIdentifier`, () => { + function runGetNormalizedIdentifierTest(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 = IdentifierUtils.getNormalizedIdentifier(text, identifierUtilsOptions); - it(`#"a""b""c"`, () => - expect(IdentifierUtils.isQuotedIdentifier(`#"a""b""c"`), "should be true").to.be.true); + if (params.expectedSuccess !== undefined) { + expect(actual).to.equal(params.expectedSuccess); + } else { + expect(actual).to.be.undefined; + } + } + + it("foo", () => { + runGetNormalizedIdentifierTest({ + text: "foo", + expectedSuccess: "foo", + }); + }); + + it("[empty string]", () => { + runGetNormalizedIdentifierTest({ + text: "", + expectedSuccess: undefined, + }); + }); + + it("foo. // allowTrailingPeriod - true", () => { + runGetNormalizedIdentifierTest({ + text: "foo.", + options: { allowTrailingPeriod: true }, + expectedSuccess: "foo.", + }); + }); + + it("foo. // allowTrailingPeriod - false", () => { + runGetNormalizedIdentifierTest({ + text: "foo.", + options: { allowTrailingPeriod: false }, + expectedSuccess: undefined, + }); + }); + + it("foo.bar", () => { + runGetNormalizedIdentifierTest({ + text: "foo.bar", + expectedSuccess: "foo.bar", + }); + }); + + it("foo.1", () => { + runGetNormalizedIdentifierTest({ + text: "foo.1", + expectedSuccess: "foo.1", + }); + }); + + it("with space // allowGeneralizedIdentifier - false", () => { + runGetNormalizedIdentifierTest({ + text: "with space", + options: { allowGeneralizedIdentifier: false }, + expectedSuccess: undefined, + }); + }); + + it("with space // allowGeneralizedIdentifier - true", () => { + runGetNormalizedIdentifierTest({ + text: "with space", + options: { allowGeneralizedIdentifier: true }, + expectedSuccess: "with space", + }); + }); + + it(`#"regularIdentifierWithUnneededQuotes" // allowGeneralizedIdentifier - false`, () => { + runGetNormalizedIdentifierTest({ + text: '#"regularIdentifierWithUnneededQuotes"', + options: { allowGeneralizedIdentifier: false }, + expectedSuccess: "regularIdentifierWithUnneededQuotes", + }); + }); - it(`#"""b""c"`, () => expect(IdentifierUtils.isQuotedIdentifier(`#"""b""c"`), "should be true").to.be.true); - it(`#"a""b"""`, () => expect(IdentifierUtils.isQuotedIdentifier(`#"a""b"""`), "should be true").to.be.true); - it(`#"bar.1"`, () => expect(IdentifierUtils.isQuotedIdentifier(`#"foo"`), "should be true").to.be.true); + it(`#"quoted regular identifier" // allowGeneralizedIdentifier - false`, () => { + runGetNormalizedIdentifierTest({ + text: '#"quoted regular identifier"', + options: { allowGeneralizedIdentifier: false }, + expectedSuccess: `#"quoted regular identifier"`, + }); }); - describe(`invalid`, () => { - it(`#"`, () => expect(IdentifierUtils.isGeneralizedIdentifier(`#"`), "should be false").to.be.false); - it(`""`, () => expect(IdentifierUtils.isGeneralizedIdentifier(`""`), "should be false").to.be.false); + it(`#"quoted generalized identifier" // allowGeneralizedIdentifier - true`, () => { + runGetNormalizedIdentifierTest({ + text: '#"quoted generalized identifier"', + options: { allowGeneralizedIdentifier: true }, + expectedSuccess: "quoted generalized identifier", + }); }); }); }); diff --git a/src/test/libraryTest/language/typeUtils/isEqualType.test.ts b/src/test/libraryTest/language/typeUtils/isEqualType.test.ts new file mode 100644 index 00000000..01babeb7 --- /dev/null +++ b/src/test/libraryTest/language/typeUtils/isEqualType.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "mocha"; +import { expect } from "chai"; + +import { Type, TypeUtils } from "../../../../powerquery-parser/language"; +import { NoOpTraceManagerInstance } from "../../../../powerquery-parser/common/trace"; +import { TPowerQueryType } from "../../../../powerquery-parser/language/type/type"; + +describe(`TypeUtils.isEqualType`, () => { + function runTest(params: { + readonly left: TPowerQueryType; + readonly right: TPowerQueryType; + readonly expected: boolean; + }): void { + const actual: boolean = TypeUtils.isEqualType(params.left, params.right); + expect(actual).to.equal(params.expected); + } + + describe(`${Type.TypeKind.Function}`, () => { + it(`null is not compatible`, () => { + const type: TPowerQueryType = TypeUtils.definedFunction( + false, + [ + { + isNullable: false, + isOptional: true, + type: Type.TypeKind.Text, + nameLiteral: `x`, + }, + ], + TypeUtils.anyUnion( + [ + { + isNullable: false, + kind: Type.TypeKind.Number, + literal: `1`, + extendedKind: Type.ExtendedTypeKind.NumberLiteral, + normalizedLiteral: 1, + }, + { + isNullable: false, + kind: Type.TypeKind.Number, + literal: `2`, + extendedKind: Type.ExtendedTypeKind.NumberLiteral, + normalizedLiteral: 2, + }, + ], + NoOpTraceManagerInstance, + undefined, + ), + ); + + runTest({ + left: type, + right: type, + expected: true, + }); + }); + }); +});