Skip to content

Commit aef0085

Browse files
perhaps complete
1 parent 6fdbe2f commit aef0085

File tree

7 files changed

+382
-131
lines changed

7 files changed

+382
-131
lines changed

src/powerquery-parser/language/identifierUtils.ts

Lines changed: 117 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,25 @@ export enum IdentifierKind {
1212
RegularWithRequiredQuotes = "RegularWithRequiredQuotes",
1313
}
1414

15-
// Assuming the text is a quoted identifier, finds the quotes that enclose the identifier.
16-
// Otherwise returns undefined.
17-
export function findQuotedIdentifierQuotes(text: string, index: number): StringUtils.FoundQuotes | undefined {
18-
if (text[index] !== "#") {
19-
return undefined;
20-
}
21-
22-
return StringUtils.findQuotes(text, index + 1);
15+
export interface IdentifierUtilsOptions {
16+
readonly allowGeneralizedIdentifier?: boolean;
17+
readonly allowTrailingPeriod?: boolean;
2318
}
2419

25-
export function getAllowedIdentifiers(text: string, isGeneralizedIdentifierAllowed: boolean): ReadonlyArray<string> {
26-
const quotedAndUnquoted: TQuotedAndUnquoted | undefined = getQuotedAndUnquoted(text);
20+
export function getAllowedIdentifiers(text: string, options?: IdentifierUtilsOptions): ReadonlyArray<string> {
21+
const allowGeneralizedIdentifier: boolean =
22+
options?.allowGeneralizedIdentifier ?? DefaultallowGeneralizedIdentifier;
23+
24+
const quotedAndUnquoted: TQuotedAndUnquoted | undefined = getQuotedAndUnquoted(text, options);
2725

2826
if (quotedAndUnquoted === undefined) {
2927
return [];
3028
}
3129

3230
switch (quotedAndUnquoted.identifierKind) {
3331
case IdentifierKind.Generalized:
34-
quotedAndUnquoted.withoutQuotes;
35-
36-
return isGeneralizedIdentifierAllowed
37-
? [quotedAndUnquoted.withQuotes, quotedAndUnquoted.withoutQuotes]
38-
: [];
32+
case IdentifierKind.GeneralizedWithQuotes:
33+
return allowGeneralizedIdentifier ? [quotedAndUnquoted.withQuotes, quotedAndUnquoted.withoutQuotes] : [];
3934

4035
case IdentifierKind.Invalid:
4136
return [];
@@ -57,23 +52,37 @@ export function getAllowedIdentifiers(text: string, isGeneralizedIdentifierAllow
5752
// Determines what kind of identifier the text is.
5853
// It's possible that the text is a partially completed identifier,
5954
// which is why we have the `allowTrailingPeriod` parameter.
60-
export function getIdentifierKind(text: string, allowTrailingPeriod: boolean): IdentifierKind {
61-
if (isRegularIdentifier(text, allowTrailingPeriod)) {
55+
export function getIdentifierKind(text: string, options?: IdentifierUtilsOptions): IdentifierKind {
56+
const allowGeneralizedIdentifier: boolean =
57+
options?.allowGeneralizedIdentifier ?? DefaultallowGeneralizedIdentifier;
58+
59+
if (isRegularIdentifier(text, options)) {
6260
return IdentifierKind.Regular;
63-
} else if (isQuotedIdentifier(text)) {
64-
if (isRegularIdentifier)
65-
return isRegularIdentifier(text.slice(2, -1), false)
66-
? IdentifierKind.RegularWithQuotes
67-
: IdentifierKind.RegularWithRequiredQuotes;
68-
} else if (isGeneralizedIdentifier(text)) {
61+
} else if (allowGeneralizedIdentifier && isGeneralizedIdentifier(text)) {
6962
return IdentifierKind.Generalized;
63+
}
64+
// If the identifier is quoted it's either:
65+
// - a regular identifier with quotes,
66+
// - a generalized identifier with quotes,
67+
else if (isQuotedIdentifier(text)) {
68+
const stripped: string = stripQuotes(text);
69+
70+
if (isRegularIdentifier(stripped, options)) {
71+
return IdentifierKind.RegularWithQuotes;
72+
} else if (isGeneralizedIdentifier(stripped) && allowGeneralizedIdentifier) {
73+
return IdentifierKind.GeneralizedWithQuotes;
74+
} else {
75+
return IdentifierKind.RegularWithRequiredQuotes;
76+
}
7077
} else {
7178
return IdentifierKind.Invalid;
7279
}
7380
}
7481

75-
// Assuming the text is an identifier, returns the length of the identifier.
76-
export function getIdentifierLength(text: string, index: number, allowTrailingPeriod: boolean): number | undefined {
82+
// I'd prefer if this was internal, but it's used by the lexer so it's marked as public.
83+
// Returns the length of the identifier starting at the given index.
84+
export function getIdentifierLength(text: string, index: number, options?: IdentifierUtilsOptions): number | undefined {
85+
const allowTrailingPeriod: boolean = options?.allowTrailingPeriod ?? DefaultAllowTrailingPeriod;
7786
const startingIndex: number = index;
7887
const textLength: number = text.length;
7988

@@ -153,8 +162,69 @@ export function getIdentifierLength(text: string, index: number, allowTrailingPe
153162
return index !== startingIndex ? index - startingIndex : undefined;
154163
}
155164

165+
// Removes the quotes from a quoted identifier if possible.
166+
export function getNormalizedIdentifier(
167+
text: string,
168+
options?: IdentifierUtilsOptions,
169+
): Result<string, CommonError.InvariantError> {
170+
const allowGeneralizedIdentifier: boolean =
171+
options?.allowGeneralizedIdentifier ?? DefaultallowGeneralizedIdentifier;
172+
173+
const quotedAndUnquoted: TQuotedAndUnquoted = getQuotedAndUnquoted(text, options);
174+
175+
if (quotedAndUnquoted.identifierKind === IdentifierKind.Invalid) {
176+
return ResultUtils.error(new CommonError.InvariantError(`The text "${text}" is not a valid identifier.`));
177+
}
178+
179+
// Validate a generalized identifier is allowed in this context.
180+
if (quotedAndUnquoted.identifierKind === IdentifierKind.Generalized && !allowGeneralizedIdentifier) {
181+
return ResultUtils.error(
182+
new CommonError.InvariantError(
183+
`The text "${text}" is a generalized identifier, but it is not allowed in this context.`,
184+
),
185+
);
186+
}
187+
188+
// Prefer without quotes if it exists.
189+
return ResultUtils.ok(quotedAndUnquoted.withoutQuotes ?? quotedAndUnquoted.withQuotes);
190+
}
191+
192+
interface IQuotedAndUnquoted<
193+
TKind extends IdentifierKind,
194+
TWithQuotes extends string | undefined,
195+
TWithoutQuotes extends string | undefined,
196+
> {
197+
readonly identifierKind: TKind;
198+
readonly withQuotes: TWithQuotes;
199+
readonly withoutQuotes: TWithoutQuotes;
200+
}
201+
202+
type TQuotedAndUnquoted =
203+
| IQuotedAndUnquoted<IdentifierKind.Generalized, string, string>
204+
| IQuotedAndUnquoted<IdentifierKind.GeneralizedWithQuotes, string, string>
205+
| IQuotedAndUnquoted<IdentifierKind.Invalid, undefined, undefined>
206+
| IQuotedAndUnquoted<IdentifierKind.RegularWithQuotes, string, string>
207+
| IQuotedAndUnquoted<IdentifierKind.RegularWithRequiredQuotes, string, undefined>
208+
| IQuotedAndUnquoted<IdentifierKind.Regular, string, string>;
209+
210+
const enum IdentifierRegexpState {
211+
Done = "Done",
212+
RegularIdentifier = "RegularIdentifier",
213+
Start = "Start",
214+
}
215+
216+
// Assuming the text is a quoted identifier, finds the quotes that enclose the identifier.
217+
// Otherwise returns undefined.
218+
function findQuotedIdentifierQuotes(text: string, index: number): StringUtils.FoundQuotes | undefined {
219+
if (text[index] !== "#") {
220+
return undefined;
221+
}
222+
223+
return StringUtils.findQuotes(text, index + 1);
224+
}
225+
156226
// Assuming the text is a generalized identifier, returns the length of the identifier.
157-
export function getGeneralizedIdentifierLength(text: string, index: number): number | undefined {
227+
function getGeneralizedIdentifierLength(text: string, index: number): number | undefined {
158228
const startingIndex: number = index;
159229
const textLength: number = text.length;
160230

@@ -195,50 +265,21 @@ export function getGeneralizedIdentifierLength(text: string, index: number): num
195265
return index !== startingIndex ? index - startingIndex : undefined;
196266
}
197267

198-
export function isGeneralizedIdentifier(text: string): boolean {
199-
return getGeneralizedIdentifierLength(text, 0) === text.length;
200-
}
201-
202-
export function isRegularIdentifier(text: string, allowTrailingPeriod: boolean): boolean {
203-
return getIdentifierLength(text, 0, allowTrailingPeriod) === text.length;
204-
}
205-
206-
export function isQuotedIdentifier(text: string): boolean {
207-
return findQuotedIdentifierQuotes(text, 0) !== undefined;
208-
}
209-
210-
// Removes the quotes from a quoted identifier if possible.
211-
export function getNormalizedIdentifier(
212-
text: string,
213-
isGeneralizedIdentifierAllowed: boolean,
214-
): Result<string, CommonError.InvariantError> {
215-
const quotedAndUnquoted: TQuotedAndUnquoted = getQuotedAndUnquoted(text);
216-
217-
if (quotedAndUnquoted.identifierKind === IdentifierKind.Invalid) {
218-
return ResultUtils.error(new CommonError.InvariantError(`The text "${text}" is not a valid identifier.`));
219-
}
220-
221-
// Validate a generalized identifier is allowed in this context.
222-
if (quotedAndUnquoted.identifierKind === IdentifierKind.Generalized && !isGeneralizedIdentifierAllowed) {
223-
return ResultUtils.error(
224-
new CommonError.InvariantError(
225-
`The text "${text}" is a generalized identifier, but it is not allowed in this context.`,
226-
),
227-
);
228-
}
229-
230-
// Prefer without quotes if it exists.
231-
return ResultUtils.ok(quotedAndUnquoted.withoutQuotes ?? quotedAndUnquoted.withQuotes);
232-
}
233-
234-
function getQuotedAndUnquoted(text: string): TQuotedAndUnquoted {
235-
const identifierKind: IdentifierKind = getIdentifierKind(text, /* allowTrailingPeriod */ false);
268+
function getQuotedAndUnquoted(text: string, options?: IdentifierUtilsOptions): TQuotedAndUnquoted {
269+
const identifierKind: IdentifierKind = getIdentifierKind(text, options);
236270

237271
switch (identifierKind) {
238272
case IdentifierKind.Generalized:
239273
return {
240274
identifierKind,
241-
withoutQuotes: insertQuotes(text),
275+
withoutQuotes: text,
276+
withQuotes: insertQuotes(text),
277+
};
278+
279+
case IdentifierKind.GeneralizedWithQuotes:
280+
return {
281+
identifierKind,
282+
withoutQuotes: stripQuotes(text),
242283
withQuotes: text,
243284
};
244285

@@ -275,38 +316,25 @@ function getQuotedAndUnquoted(text: string): TQuotedAndUnquoted {
275316
}
276317
}
277318

278-
interface IQuotedAndUnquoted<
279-
TKind extends IdentifierKind,
280-
TWithQuotes extends string | undefined,
281-
TWithoutQuotes extends string | undefined,
282-
> {
283-
readonly identifierKind: TKind;
284-
readonly withQuotes: TWithQuotes;
285-
readonly withoutQuotes: TWithoutQuotes;
319+
function insertQuotes(text: string): string {
320+
return `#"${text}"`;
286321
}
287322

288-
type TQuotedAndUnquoted =
289-
| IQuotedAndUnquoted<IdentifierKind.Generalized, string, string>
290-
| IQuotedAndUnquoted<IdentifierKind.Invalid, undefined, undefined>
291-
| IQuotedAndUnquoted<IdentifierKind.RegularWithQuotes, string, string>
292-
| IQuotedAndUnquoted<IdentifierKind.RegularWithRequiredQuotes, string, undefined>
293-
| IQuotedAndUnquoted<IdentifierKind.Regular, string, string>;
323+
function isGeneralizedIdentifier(text: string): boolean {
324+
return text.length > 0 && getGeneralizedIdentifierLength(text, 0) === text.length;
325+
}
294326

295-
const enum IdentifierRegexpState {
296-
Done = "Done",
297-
RegularIdentifier = "RegularIdentifier",
298-
Start = "Start",
327+
function isRegularIdentifier(text: string, options?: IdentifierUtilsOptions): boolean {
328+
return text.length > 0 && getIdentifierLength(text, 0, options) === text.length;
299329
}
300330

301-
function insertQuotes(text: string): string {
302-
return `#"${text}"`;
331+
function isQuotedIdentifier(text: string): boolean {
332+
return findQuotedIdentifierQuotes(text, 0) !== undefined;
303333
}
304334

305335
function stripQuotes(text: string): string {
306336
return text.slice(2, -1);
307337
}
308338

309-
interface IdentifierUtilsOptions {
310-
readonly allowTrailingPeriod?: boolean;
311-
readonly isGeneralizedIdentifierAllowed?: boolean;
312-
}
339+
const DefaultAllowTrailingPeriod: boolean = false;
340+
const DefaultallowGeneralizedIdentifier: boolean = false;

src/powerquery-parser/language/type/typeUtils/isEqualType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function isEqualType(left: Type.TPowerQueryType, right: Type.TPowerQueryT
2222
}
2323

2424
export function isEqualFunctionParameter(left: Type.FunctionParameter, right: Type.FunctionParameter): boolean {
25-
return left.isNullable !== right.isNullable || left.isOptional !== right.isOptional || left.type !== right.type;
25+
return left.isNullable === right.isNullable && left.isOptional === right.isOptional && left.type === right.type;
2626
}
2727

2828
export function isEqualFunctionSignature(

src/powerquery-parser/lexer/lexer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,6 @@ function readKeyword(text: string, lineNumber: number, positionStart: number, lo
10151015

10161016
function readKeywordHelper(text: string, currentPosition: number): Token.LineToken | undefined {
10171017
const identifierPositionStart: number = text[currentPosition] === "#" ? currentPosition + 1 : currentPosition;
1018-
10191018
const identifierPositionEnd: number | undefined = indexOfIdentifierEnd(text, identifierPositionStart);
10201019

10211020
if (identifierPositionEnd === undefined) {
@@ -1137,7 +1136,9 @@ function indexOfRegexEnd(pattern: RegExp, text: string, positionStart: number):
11371136
}
11381137

11391138
function indexOfIdentifierEnd(text: string, positionStart: number): number | undefined {
1140-
const length: number | undefined = IdentifierUtils.getIdentifierLength(text, positionStart, true);
1139+
const length: number | undefined = IdentifierUtils.getIdentifierLength(text, positionStart, {
1140+
allowTrailingPeriod: true,
1141+
});
11411142

11421143
return length !== undefined ? positionStart + length : undefined;
11431144
}

src/powerquery-parser/parser/nodeIdMap/nodeIdMapIterator.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ export function iterFieldSpecificationList(
343343
optional,
344344
value,
345345
normalizedKeyLiteral: ResultUtils.assertOk(
346-
IdentifierUtils.getNormalizedIdentifier(keyLiteral, /* isGeneralizedIdentifierAllowed */ true),
346+
IdentifierUtils.getNormalizedIdentifier(keyLiteral, { allowGeneralizedIdentifier: true }),
347347
),
348348
pairKind: PairKind.FieldSpecification,
349349
source: fieldSpecification,
@@ -382,7 +382,7 @@ export function iterLetExpression(
382382
nodeIdMapCollection,
383383
arrayWrapper,
384384
PairKind.LetExpression,
385-
/* isGeneralizedIdentifierAllowed */ false,
385+
{ allowGeneralizedIdentifier: false },
386386
);
387387
}
388388

@@ -413,7 +413,7 @@ export function iterRecord(
413413
nodeIdMapCollection,
414414
arrayWrapper,
415415
PairKind.Record,
416-
/* isGeneralizedIdentifierAllowed */ true,
416+
{ allowGeneralizedIdentifier: true },
417417
);
418418
}
419419

@@ -455,7 +455,7 @@ export function iterSection(
455455
key: namePairedExpression.key,
456456
keyLiteral,
457457
normalizedKeyLiteral: ResultUtils.assertOk(
458-
IdentifierUtils.getNormalizedIdentifier(keyLiteral, /* isGeneralizedIdentifierAllowed */ true),
458+
IdentifierUtils.getNormalizedIdentifier(keyLiteral, { allowGeneralizedIdentifier: true }),
459459
),
460460
value: XorNodeUtils.boxAst(namePairedExpression.value),
461461
pairKind: PairKind.SectionMember,
@@ -511,7 +511,7 @@ export function iterSection(
511511
key,
512512
keyLiteral,
513513
normalizedKeyLiteral: ResultUtils.assertOk(
514-
IdentifierUtils.getNormalizedIdentifier(keyLiteral, /* isGeneralizedIdentifierAllowed */ true),
514+
IdentifierUtils.getNormalizedIdentifier(keyLiteral, { allowGeneralizedIdentifier: true }),
515515
),
516516
value: NodeIdMapUtils.nthChildXor(nodeIdMapCollection, keyValuePairNodeId, 2),
517517
pairKind: PairKind.SectionMember,
@@ -528,7 +528,7 @@ function iterKeyValuePairs<
528528
nodeIdMapCollection: NodeIdMap.Collection,
529529
arrayWrapper: TXorNode,
530530
pairKind: TKeyValuePair["pairKind"],
531-
isGeneralizedIdentifierAllowed: boolean,
531+
identifierUtilsOptions: IdentifierUtils.IdentifierUtilsOptions,
532532
): ReadonlyArray<KVP> {
533533
const partial: KVP[] = [];
534534

@@ -549,7 +549,7 @@ function iterKeyValuePairs<
549549
key,
550550
keyLiteral,
551551
normalizedKeyLiteral: ResultUtils.assertOk(
552-
IdentifierUtils.getNormalizedIdentifier(keyLiteral, isGeneralizedIdentifierAllowed),
552+
IdentifierUtils.getNormalizedIdentifier(keyLiteral, identifierUtilsOptions),
553553
),
554554
value: NodeIdMapUtils.nthChildXor(nodeIdMapCollection, keyValuePair.node.id, 2),
555555
pairKind,

src/powerquery-parser/parser/parsers/naiveParseSteps.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,11 @@ export async function readGeneralizedIdentifier(
121121
const contiguousIdentifierStartIndex: number = tokens[tokenRangeStartIndex].positionStart.codeUnit;
122122
const contiguousIdentifierEndIndex: number = tokens[tokenRangeEndIndex - 1].positionEnd.codeUnit;
123123
const literal: string = lexerSnapshot.text.slice(contiguousIdentifierStartIndex, contiguousIdentifierEndIndex);
124-
const literalKind: IdentifierUtils.IdentifierKind = IdentifierUtils.getIdentifierKind(literal, true);
124+
125+
const literalKind: IdentifierUtils.IdentifierKind = IdentifierUtils.getIdentifierKind(literal, {
126+
allowTrailingPeriod: true,
127+
allowGeneralizedIdentifier: true,
128+
});
125129

126130
if (literalKind === IdentifierUtils.IdentifierKind.Invalid) {
127131
trace.exit({

0 commit comments

Comments
 (0)