@@ -5,39 +5,114 @@ import { Assert, Pattern, StringUtils } from "../common";
5
5
6
6
export enum IdentifierKind {
7
7
Generalized = "Generalized" ,
8
+ GeneralizedWithQuotes = "GeneralizedWithQuotes" ,
8
9
Invalid = "Invalid" ,
9
- Quote = "Quote" ,
10
- QuoteRequired = "QuoteRequired" ,
11
10
Regular = "Regular" ,
11
+ RegularWithQuotes = "RegularWithQuotes" ,
12
+ RegularWithRequiredQuotes = "RegularWithRequiredQuotes" ,
12
13
}
13
14
14
- // Assuming the text is a quoted identifier, finds the quotes that enclose the identifier.
15
- // Otherwise returns undefined.
16
- export function findQuotedIdentifierQuotes ( text : string , index : number ) : StringUtils . FoundQuotes | undefined {
17
- if ( text [ index ] !== "#" ) {
18
- return undefined ;
15
+ export interface CommonIdentifierUtilsOptions {
16
+ readonly allowGeneralizedIdentifier ?: boolean ;
17
+ readonly allowTrailingPeriod ?: boolean ;
18
+ }
19
+
20
+ export interface GetAllowedIdentifiersOptions extends CommonIdentifierUtilsOptions {
21
+ readonly allowRecursive ?: boolean ;
22
+ }
23
+
24
+ // Identifiers have multiple forms that can be used interchangeably.
25
+ // For example, if you have `[key = 1]`, you can use `key` or `#""key""`.
26
+ // The `getAllowedIdentifiers` function returns all the forms of the identifier that are allowed in the current context.
27
+ export function getAllowedIdentifiers ( text : string , options ?: GetAllowedIdentifiersOptions ) : ReadonlyArray < string > {
28
+ const allowGeneralizedIdentifier : boolean =
29
+ options ?. allowGeneralizedIdentifier ?? DefaultAllowGeneralizedIdentifier ;
30
+
31
+ const quotedAndUnquoted : TQuotedAndUnquoted | undefined = getQuotedAndUnquoted ( text , options ) ;
32
+
33
+ if ( quotedAndUnquoted === undefined ) {
34
+ return [ ] ;
19
35
}
20
36
21
- return StringUtils . findQuotes ( text , index + 1 ) ;
37
+ let result : string [ ] ;
38
+
39
+ switch ( quotedAndUnquoted . identifierKind ) {
40
+ case IdentifierKind . Generalized :
41
+ case IdentifierKind . GeneralizedWithQuotes :
42
+ result = allowGeneralizedIdentifier ? [ quotedAndUnquoted . withQuotes , quotedAndUnquoted . withoutQuotes ] : [ ] ;
43
+ break ;
44
+
45
+ case IdentifierKind . Invalid :
46
+ result = [ ] ;
47
+ break ;
48
+
49
+ case IdentifierKind . RegularWithQuotes :
50
+ result = [ quotedAndUnquoted . withQuotes , quotedAndUnquoted . withoutQuotes ] ;
51
+ break ;
52
+
53
+ case IdentifierKind . RegularWithRequiredQuotes :
54
+ result = [ quotedAndUnquoted . withQuotes ] ;
55
+ break ;
56
+
57
+ case IdentifierKind . Regular :
58
+ result = [ quotedAndUnquoted . withoutQuotes , quotedAndUnquoted . withQuotes ] ;
59
+ break ;
60
+
61
+ default :
62
+ throw Assert . isNever ( quotedAndUnquoted ) ;
63
+ }
64
+
65
+ if ( options ?. allowRecursive ) {
66
+ result = result . concat ( result . map ( ( value : string ) => prefixInclusiveConstant ( value ) ) ) ;
67
+ }
68
+
69
+ return result ;
22
70
}
23
71
24
- // Determines what kind of identifier the text is.
25
- // It's possible that the text is a partially completed identifier,
26
- // which is why we have the `allowTrailingPeriod` parameter.
27
- export function getIdentifierKind ( text : string , allowTrailingPeriod : boolean ) : IdentifierKind {
28
- if ( isRegularIdentifier ( text , allowTrailingPeriod ) ) {
72
+ // An identifier can have multiple forms:
73
+ // - Regular: `foo`
74
+ // - Regular with quotes: `#""foo""`
75
+ // - Regular with required quotes: `#""foo bar""`
76
+ // - Regular with required quotes is used when the identifier has spaces or special characters,
77
+ // and when generalized identifiers are not allowed.
78
+ // - Generalized: `foo bar`
79
+ // - Generalized with quotes: `#""foo bar""`
80
+ // - Invalid: `foo..bar`
81
+ export function getIdentifierKind ( text : string , options ?: CommonIdentifierUtilsOptions ) : IdentifierKind {
82
+ const allowGeneralizedIdentifier : boolean =
83
+ options ?. allowGeneralizedIdentifier ?? DefaultAllowGeneralizedIdentifier ;
84
+
85
+ if ( isRegularIdentifier ( text , options ) ) {
29
86
return IdentifierKind . Regular ;
30
- } else if ( isQuotedIdentifier ( text ) ) {
31
- return isRegularIdentifier ( text . slice ( 2 , - 1 ) , false ) ? IdentifierKind . Quote : IdentifierKind . QuoteRequired ;
32
- } else if ( isGeneralizedIdentifier ( text ) ) {
87
+ } else if ( allowGeneralizedIdentifier && isGeneralizedIdentifier ( text ) ) {
33
88
return IdentifierKind . Generalized ;
89
+ }
90
+ // If the identifier is quoted it's either:
91
+ // - a regular identifier with quotes,
92
+ // - a generalized identifier with quotes,
93
+ else if ( isQuotedIdentifier ( text ) ) {
94
+ const stripped : string = stripQuotes ( text ) ;
95
+
96
+ if ( isRegularIdentifier ( stripped , options ) ) {
97
+ return IdentifierKind . RegularWithQuotes ;
98
+ } else if ( isGeneralizedIdentifier ( stripped ) && allowGeneralizedIdentifier ) {
99
+ return IdentifierKind . GeneralizedWithQuotes ;
100
+ } else {
101
+ return IdentifierKind . RegularWithRequiredQuotes ;
102
+ }
34
103
} else {
35
104
return IdentifierKind . Invalid ;
36
105
}
37
106
}
38
107
39
- // Assuming the text is an identifier, returns the length of the identifier.
40
- export function getIdentifierLength ( text : string , index : number , allowTrailingPeriod : boolean ) : number | undefined {
108
+ // I'd prefer if this was internal, but it's used by the lexer so it's marked as public.
109
+ // Returns the length of the identifier starting at the given index.
110
+ export function getIdentifierLength (
111
+ text : string ,
112
+ index : number ,
113
+ options ?: CommonIdentifierUtilsOptions ,
114
+ ) : number | undefined {
115
+ const allowTrailingPeriod : boolean = options ?. allowTrailingPeriod ?? DefaultAllowTrailingPeriod ;
41
116
const startingIndex : number = index ;
42
117
const textLength : number = text . length ;
43
118
@@ -62,26 +137,37 @@ export function getIdentifierLength(text: string, index: number, allowTrailingPe
62
137
63
138
break ;
64
139
65
- case IdentifierRegexpState . RegularIdentifier :
66
- // Don't consider `..` or `...` part of an identifier .
67
- if ( allowTrailingPeriod && text [ index ] === "." && text [ index + 1 ] !== "." ) {
68
- index += 1 ;
69
- }
140
+ // We should allow a single period as part of the identifier,
141
+ // but only if it's not the last character and not followed by another period .
142
+ // Allow an exception for when it's the last character and allowTrailingPeriod is true.
143
+ case IdentifierRegexpState . RegularIdentifier : {
144
+ const currentChr : string | undefined = text [ index ] ;
70
145
71
- matchLength = StringUtils . regexMatchLength ( Pattern . IdentifierPartCharacters , text , index ) ;
72
-
73
- if ( matchLength === undefined ) {
146
+ if ( currentChr === undefined ) {
74
147
state = IdentifierRegexpState . Done ;
75
- } else {
76
- index += matchLength ;
148
+ } else if ( currentChr === "." ) {
149
+ const nextChr : string | undefined = text [ index + 1 ] ;
77
150
78
- // Don't consider `..` or `...` part of an identifier.
79
- if ( allowTrailingPeriod && text [ index ] === "." && text [ index + 1 ] !== "." ) {
151
+ // If we have a single period we might include it as part of the identifier when:
152
+ // 1. It's not the last character and not followed by another period
153
+ // 2. It's the last character and allowTrailingPeriod is true
154
+ if ( ( nextChr && nextChr !== "." ) || ( nextChr === undefined && allowTrailingPeriod ) ) {
80
155
index += 1 ;
156
+ } else {
157
+ state = IdentifierRegexpState . Done ;
158
+ }
159
+ } else {
160
+ matchLength = StringUtils . regexMatchLength ( Pattern . IdentifierPartCharacters , text , index ) ;
161
+
162
+ if ( matchLength === undefined ) {
163
+ state = IdentifierRegexpState . Done ;
164
+ } else {
165
+ index += matchLength ;
81
166
}
82
167
}
83
168
84
169
break ;
170
+ }
85
171
86
172
default :
87
173
throw Assert . isNever ( state ) ;
@@ -91,8 +177,81 @@ export function getIdentifierLength(text: string, index: number, allowTrailingPe
91
177
return index !== startingIndex ? index - startingIndex : undefined ;
92
178
}
93
179
180
+ // Removes the quotes from a quoted identifier if possible.
181
+ // When given an invalid identifier, returns undefined.
182
+ export function getNormalizedIdentifier ( text : string , options ?: CommonIdentifierUtilsOptions ) : string | undefined {
183
+ const allowGeneralizedIdentifier : boolean =
184
+ options ?. allowGeneralizedIdentifier ?? DefaultAllowGeneralizedIdentifier ;
185
+
186
+ const quotedAndUnquoted : TQuotedAndUnquoted = getQuotedAndUnquoted ( text , options ) ;
187
+
188
+ switch ( quotedAndUnquoted . identifierKind ) {
189
+ case IdentifierKind . Regular :
190
+ case IdentifierKind . RegularWithQuotes :
191
+ return quotedAndUnquoted . withoutQuotes ;
192
+
193
+ case IdentifierKind . GeneralizedWithQuotes :
194
+ case IdentifierKind . Generalized :
195
+ return allowGeneralizedIdentifier ? quotedAndUnquoted . withoutQuotes : undefined ;
196
+
197
+ case IdentifierKind . Invalid :
198
+ return undefined ;
199
+
200
+ case IdentifierKind . RegularWithRequiredQuotes :
201
+ return quotedAndUnquoted . withQuotes ;
202
+
203
+ default :
204
+ throw Assert . isNever ( quotedAndUnquoted ) ;
205
+ }
206
+ }
207
+
208
+ type TQuotedAndUnquoted =
209
+ | {
210
+ readonly identifierKind : IdentifierKind . Generalized ;
211
+ readonly withQuotes : string ;
212
+ readonly withoutQuotes : string ;
213
+ }
214
+ | {
215
+ readonly identifierKind : IdentifierKind . GeneralizedWithQuotes ;
216
+ readonly withQuotes : string ;
217
+ readonly withoutQuotes : string ;
218
+ }
219
+ | {
220
+ readonly identifierKind : IdentifierKind . Invalid ;
221
+ }
222
+ | {
223
+ readonly identifierKind : IdentifierKind . Regular ;
224
+ readonly withQuotes : string ;
225
+ readonly withoutQuotes : string ;
226
+ }
227
+ | {
228
+ readonly identifierKind : IdentifierKind . RegularWithQuotes ;
229
+ readonly withQuotes : string ;
230
+ readonly withoutQuotes : string ;
231
+ }
232
+ | {
233
+ readonly identifierKind : IdentifierKind . RegularWithRequiredQuotes ;
234
+ readonly withQuotes : string ;
235
+ } ;
236
+
237
+ const enum IdentifierRegexpState {
238
+ Done = "Done" ,
239
+ RegularIdentifier = "RegularIdentifier" ,
240
+ Start = "Start" ,
241
+ }
242
+
243
+ // Finds the locations of quotes in a quoted identifier.
244
+ // Returns undefined if the identifier is not quoted.
245
+ function findQuotedIdentifierQuotes ( text : string , index : number ) : StringUtils . FoundQuotes | undefined {
246
+ if ( text [ index ] !== "#" ) {
247
+ return undefined ;
248
+ }
249
+
250
+ return StringUtils . findQuotes ( text , index + 1 ) ;
251
+ }
252
+
94
253
// Assuming the text is a generalized identifier, returns the length of the identifier.
95
- export function getGeneralizedIdentifierLength ( text : string , index : number ) : number | undefined {
254
+ function getGeneralizedIdentifierLength ( text : string , index : number ) : number | undefined {
96
255
const startingIndex : number = index ;
97
256
const textLength : number = text . length ;
98
257
@@ -133,31 +292,78 @@ export function getGeneralizedIdentifierLength(text: string, index: number): num
133
292
return index !== startingIndex ? index - startingIndex : undefined ;
134
293
}
135
294
136
- export function isGeneralizedIdentifier ( text : string ) : boolean {
137
- return getGeneralizedIdentifierLength ( text , 0 ) === text . length ;
295
+ // Returns the quoted and unquoted versions of the identifier (if applicable).
296
+ function getQuotedAndUnquoted ( text : string , options ?: CommonIdentifierUtilsOptions ) : TQuotedAndUnquoted {
297
+ const identifierKind : IdentifierKind = getIdentifierKind ( text , options ) ;
298
+
299
+ switch ( identifierKind ) {
300
+ case IdentifierKind . Generalized :
301
+ return {
302
+ identifierKind,
303
+ withoutQuotes : text ,
304
+ withQuotes : makeQuoted ( text ) ,
305
+ } ;
306
+
307
+ case IdentifierKind . GeneralizedWithQuotes :
308
+ return {
309
+ identifierKind,
310
+ withoutQuotes : stripQuotes ( text ) ,
311
+ withQuotes : text ,
312
+ } ;
313
+
314
+ case IdentifierKind . Invalid :
315
+ return {
316
+ identifierKind,
317
+ } ;
318
+
319
+ case IdentifierKind . RegularWithQuotes :
320
+ return {
321
+ identifierKind,
322
+ withoutQuotes : stripQuotes ( text ) ,
323
+ withQuotes : text ,
324
+ } ;
325
+
326
+ case IdentifierKind . RegularWithRequiredQuotes :
327
+ return {
328
+ identifierKind,
329
+ withQuotes : text ,
330
+ } ;
331
+
332
+ case IdentifierKind . Regular :
333
+ return {
334
+ identifierKind,
335
+ withoutQuotes : text ,
336
+ withQuotes : makeQuoted ( text ) ,
337
+ } ;
338
+
339
+ default :
340
+ throw Assert . isNever ( identifierKind ) ;
341
+ }
138
342
}
139
343
140
- export function isRegularIdentifier ( text : string , allowTrailingPeriod : boolean ) : boolean {
141
- return getIdentifierLength ( text , 0 , allowTrailingPeriod ) === text . length ;
344
+ function makeQuoted ( text : string ) : string {
345
+ return `#" ${ text } "` ;
142
346
}
143
347
144
- export function isQuotedIdentifier ( text : string ) : boolean {
145
- return findQuotedIdentifierQuotes ( text , 0 ) !== undefined ;
348
+ function prefixInclusiveConstant ( text : string ) : string {
349
+ return `@ ${ text } ` ;
146
350
}
147
351
148
- // Removes the quotes from a quoted identifier if possible.
149
- export function normalizeIdentifier ( text : string ) : string {
150
- if ( isQuotedIdentifier ( text ) ) {
151
- const stripped : string = text . slice ( 2 , - 1 ) ;
352
+ function isGeneralizedIdentifier ( text : string ) : boolean {
353
+ return text . length > 0 && getGeneralizedIdentifierLength ( text , 0 ) === text . length ;
354
+ }
152
355
153
- return isRegularIdentifier ( stripped , false ) ? stripped : text ;
154
- } else {
155
- return text ;
156
- }
356
+ function isRegularIdentifier ( text : string , options ?: CommonIdentifierUtilsOptions ) : boolean {
357
+ return text . length > 0 && getIdentifierLength ( text , 0 , options ) === text . length ;
157
358
}
158
359
159
- const enum IdentifierRegexpState {
160
- Done = "Done" ,
161
- RegularIdentifier = "RegularIdentifier" ,
162
- Start = "Start" ,
360
+ function isQuotedIdentifier ( text : string ) : boolean {
361
+ return findQuotedIdentifierQuotes ( text , 0 ) !== undefined ;
362
+ }
363
+
364
+ function stripQuotes ( text : string ) : string {
365
+ return text . slice ( 2 , - 1 ) ;
163
366
}
367
+
368
+ const DefaultAllowTrailingPeriod : boolean = false ;
369
+ const DefaultAllowGeneralizedIdentifier : boolean = false ;
0 commit comments