-
Notifications
You must be signed in to change notification settings - Fork 470
/
Copy pathlanguage.ts
557 lines (521 loc) · 18 KB
/
language.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
import CodeGenerator from "apollo-codegen-core/lib/utilities/CodeGenerator";
import {
join as _join,
wrap as _wrap,
} from "apollo-codegen-core/lib/utilities/printing";
export interface Class {
className: string;
modifiers: string[];
superClass?: string;
adoptedProtocols?: string[];
}
export interface Struct {
structName: string;
adoptedProtocols?: string[];
description?: string;
namespace?: string;
}
export interface Protocol {
protocolName: string;
adoptedProtocols?: string[];
}
export interface Property {
propertyName: string;
typeName: string;
isOptional?: boolean;
description?: string;
}
/**
* Swift identifiers that are keywords
*
* Some of these are context-dependent and can be used as identifiers outside of the relevant
* context. As we don't understand context, we will treat them as keywords in all contexts.
*
* This list does not include keywords that aren't identifiers, such as `#available`.
*/
// prettier-ignore
const reservedKeywords = new Set([
// https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID413
// Keywords used in declarations
'associatedtype', 'class', 'deinit', 'enum', 'extension', 'fileprivate',
'func', 'import', 'init', 'inout', 'internal', 'let', 'open', 'operator',
'private', 'protocol', 'public', 'static', 'struct', 'subscript',
'typealias', 'var',
// Keywords used in statements
'break', 'case', 'continue', 'default', 'defer', 'do', 'else', 'fallthrough',
'for', 'guard', 'if', 'in', 'repeat', 'return', 'switch', 'where', 'while',
// Keywords used in expressions and types
'as', 'Any', 'catch', 'false', 'is', 'nil', 'rethrows', 'super', 'self',
'Self', 'throw', 'throws', 'true', 'try',
// Keywords used in patterns
'_',
// Keywords reserved in particular contexts
'associativity', 'convenience', 'dynamic', 'didSet', 'final', 'get', 'infix',
'indirect', 'lazy', 'left', 'mutating', 'none', 'nonmutating', 'optional',
'override', 'postfix', 'precedence', 'prefix', 'Protocol', 'required',
'right', 'set', 'Type', 'unowned', 'weak', 'willSet'
]);
/**
* Swift identifiers that are keywords in member position
*
* This is the subset of keywords that are known to still be keywords in member position. The
* documentation is not explicit about which keywords qualify, but these are the ones that are
* known to have meaning in member position.
*
* We use this to avoid unnecessary escaping with expressions like `.public`.
*/
const reservedMemberKeywords = new Set(["self", "Type", "Protocol"]);
/**
* A class that represents Swift source.
*
* Instances of this type will not undergo escaping when used with the `swift` template tag.
*/
export class SwiftSource {
source: string;
constructor(source: string) {
this.source = source;
}
/**
* Returns the input wrapped in quotes and escaped appropriately.
* @param string The input string, to be represented as a Swift string.
* @param trim If true, trim the string of whitespace and join into a single line.
* @returns A `SwiftSource` containing the Swift string literal.
*/
static string(string: string, trim: boolean = false): SwiftSource {
if (trim) {
string = string
.split(/\n/g)
.map((line) => line.trim())
.join(" ");
}
return new SwiftSource(
// String literal grammar:
// https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID417
// Technically we only need to escape ", \, newline, and carriage return, but as Swift
// defines escapes for NUL and horizontal tab, it produces nicer output to escape those as
// well.
`"${string.replace(/[\0\\\t\n\r"]/g, (c) => {
switch (c) {
case "\0":
return "\\0";
case "\t":
return "\\t";
case "\n":
return "\\n";
case "\r":
return "\\r";
default:
return `\\${c}`;
}
})}"`
);
}
/**
* Returns the input wrapped in a Swift multiline string with escaping.
* @param string The input string, to be represented as a Swift multiline string.
* @returns A `SwiftSource` containing the Swift multiline string literal.
*/
static multilineString(string: string): SwiftSource {
let rawCount = 0;
if (/"""|\\/.test(string)) {
// There's a """ (which would need escaping) or a backslash. Let's do a raw string literal instead.
// We can't just assume a single # is sufficient as it's possible to include the tokens `"""#` or
// `\#n` in a GraphQL multiline string so let's look for those.
let re = /"""(#+)|\\(#+)/g;
for (let ary = re.exec(string); ary !== null; ary = re.exec(string)) {
rawCount = Math.max(
rawCount,
(ary[1] || "").length,
(ary[2] || "").length
);
}
rawCount += 1; // add 1 to get whatever won't collide with the string
}
const rawToken = "#".repeat(rawCount);
return new SwiftSource(
`${rawToken}"""\n${string.replace(/[\0\r]/g, (c) => {
// Even in a raw string, we want to escape a couple of characters.
// It would be exceedingly weird to have these, but we can still handle them.
switch (c) {
case "\0":
return `\\${rawToken}0`;
case "\r":
return `\\${rawToken}r`;
default:
return c;
}
})}\n"""${rawToken}`
);
}
/**
* Escapes the input if it contains a reserved keyword.
*
* For example, the input `Self?` requires escaping or it will match the keyword `Self`.
*
* @param identifier The input containing identifiers to escape.
* @returns The input with all identifiers escaped.
*/
static identifier(input: string): SwiftSource {
// Swift identifiers use a significantly more complicated definition, but GraphQL names are
// limited to ASCII, so we only have to worry about ASCII strings here.
return new SwiftSource(
input.replace(/[a-zA-Z_][a-zA-Z0-9_]*/g, (match, offset, fullString) => {
if (reservedKeywords.has(match)) {
// If this keyword comes after a '.' make sure it's also a reservedMemberKeyword.
if (
offset == 0 ||
fullString[offset - 1] !== "." ||
reservedMemberKeywords.has(match)
) {
return `\`${match}\``;
}
}
return match;
})
);
}
/**
* Escapes the input if it begins with a reserved keyword not valid in member position.
*
* Most keywords are valid in member position (e.g. after a period), but a few aren't. This
* method escapes just those keywords not valid in member position, and therefore must only be
* used on input that is guaranteed to come after a dot.
* @param input The input containing identifiers to escape.
* @returns The input with relevant identifiers escaped.
*/
static memberName(input: string): SwiftSource {
return new SwiftSource(
// This behaves nearly identically to `SwiftSource.identifier` except for the logic around
// offset zero, but it's structured a bit differently to optimize for the fact that most
// matched identifiers are at offset zero.
input.replace(/[a-zA-Z_][a-zA-Z0-9_]*/g, (match, offset, fullString) => {
if (!reservedMemberKeywords.has(match)) {
// If we're not at offset 0 and not after a period, check the full set.
if (
offset == 0 ||
fullString[offset - 1] === "." ||
!reservedKeywords.has(match)
) {
return match;
}
}
return `\`${match}\``;
})
);
}
/**
* Returns whether the given name is valid as a method parameter name.
*
* Certain tokens aren't valid as method parameter names, even when escaped with backticks, as
* the compiler interprets the keyword and identifier as the same thing. In particular, `self`
* works this way.
* @param input The proposed parameter name.
* @returns `true` if the name can be used, or `false` if it needs a separate internal parameter
* name.
*/
static isValidParameterName(input: string): boolean {
// Right now `self` is the only known token that we can't use with escaping.
return input !== "self";
}
/**
* Template tag for producing a `SwiftSource` value without performing escaping.
*
* This is identical to evaluating the template without the tag and passing the result to `new
* SwiftSource(…)`.
*/
static raw(
literals: TemplateStringsArray,
...placeholders: any[]
): SwiftSource {
// We can't just evaluate the original template directly, but we can replicate its semantics.
// NB: The semantics of untagged template literals matches String.prototype.concat rather than
// the + operator. Since String.prototype.concat is documented as slower than the + operator,
// we'll just use individual template strings to do the concatenation.
var result = literals[0];
placeholders.forEach((value, i) => {
result += `${value}${literals[i + 1]}`;
});
return new SwiftSource(result);
}
toString(): string {
return this.source;
}
/**
* Concatenates multiple `SwiftSource`s together.
*/
concat(...sources: SwiftSource[]): SwiftSource {
// Documentation says + is faster than String.concat, so let's use that
return new SwiftSource(
sources.reduce((accum, value) => accum + value.source, this.source)
);
}
/**
* Appends one or more `SwiftSource`s to the end of a `SwiftSource`.
* @param sources The `SwiftSource`s to append to the end.
*/
append(...sources: SwiftSource[]) {
for (let value of sources) {
this.source += value.source;
}
}
/**
* If maybeSource is not undefined or empty, then wrap with start and end, otherwise return
* undefined.
*
* This is largely just a wrapper for `wrap()` from apollo-codegen-core/lib/utilities/printing.
*/
static wrap(
start: SwiftSource,
maybeSource?: SwiftSource,
end?: SwiftSource
): SwiftSource | undefined {
const result = _wrap(
start.source,
maybeSource !== undefined ? maybeSource.source : undefined,
end !== undefined ? end.source : undefined
);
return result ? new SwiftSource(result) : undefined;
}
/**
* Given maybeArray, return undefined if it is undefined or empty, otherwise return all items
* together separated by separator if provided.
*
* This is largely just a wrapper for `join()` from apollo-codegen-core/lib/utilities/printing.
*
* @param separator The separator to put between elements. This is typed as `string` with the
* expectation that it's generally something like `', '` but if it contains identifiers it should
* be escaped.
*/
static join(
maybeArray?: (SwiftSource | undefined)[],
separator?: string
): SwiftSource | undefined {
const result = _join(maybeArray, separator);
return result ? new SwiftSource(result) : undefined;
}
}
/**
* Template tag for producing a `SwiftSource` value by escaping expressions.
*
* All interpolated expressions will undergo identifier escaping unless the expression value is of
* type `SwiftSource`. If any interpolated expressions are actually intended as string literals, use
* the `SwiftSource.string()` function on the expression.
*/
export function swift(
literals: TemplateStringsArray,
...placeholders: any[]
): SwiftSource {
let result = literals[0];
placeholders.forEach((value, i) => {
result += _escape(value);
result += literals[i + 1];
});
return new SwiftSource(result);
}
function _escape(value: any): string {
if (value instanceof SwiftSource) {
return value.source;
} else if (typeof value === "string") {
return SwiftSource.identifier(value).source;
} else if (Array.isArray(value)) {
// I don't know why you'd be interpolating an array, but let's recurse into it.
return value.map(_escape).join();
} else if (typeof value === "object") {
// use `${…}` instead of toString to preserve string conversion semantics from untagged
// template literals.
return SwiftSource.identifier(`${value}`).source;
} else if (value === undefined) {
return "";
} else {
// Other primitives don't need to be escaped.
return `${value}`;
}
}
// Convenience accessors for wrap/join
const { wrap, join } = SwiftSource;
export class SwiftGenerator<Context> extends CodeGenerator<
Context,
{ typeName: string },
SwiftSource
> {
constructor(context: Context) {
super(context);
}
/**
* Outputs a multi-line string
*
* @param string - The Multi-lined string to output
* @param suppressMultilineStringLiterals - If true, will output the multiline string as a single trimmed
* string to save bandwidth.
* NOTE: String trimming will be disabled if the string contains a
* `"""` sequence as whitespace is significant in GraphQL multiline
* strings.
*/
multilineString(string: string, suppressMultilineStringLiterals: Boolean) {
if (suppressMultilineStringLiterals) {
this.printOnNewline(
SwiftSource.string(string, /* trim */ !string.includes('"""'))
);
} else {
SwiftSource.multilineString(string)
.source.split("\n")
.forEach((line) => {
this.printOnNewline(new SwiftSource(line));
});
}
}
comment(comment?: string, trim: Boolean = true) {
comment &&
comment.split("\n").forEach((line) => {
this.printOnNewline(SwiftSource.raw`/// ${trim ? line.trim() : line}`);
});
}
deprecationAttributes(
isDeprecated: boolean | undefined,
deprecationReason: string | undefined
) {
if (isDeprecated !== undefined && isDeprecated) {
deprecationReason =
deprecationReason !== undefined && deprecationReason.length > 0
? deprecationReason
: "";
this.printOnNewline(
swift`@available(*, deprecated, message: ${SwiftSource.string(
deprecationReason,
/* trim */ true
)})`
);
}
}
namespaceDeclaration(namespace: string | undefined, closure: Function) {
if (namespace) {
this.printNewlineIfNeeded();
this.printOnNewline(SwiftSource.raw`/// ${namespace} namespace`);
this.printOnNewline(swift`public enum ${namespace}`);
this.pushScope({ typeName: namespace });
this.withinBlock(closure);
this.popScope();
} else {
if (closure) {
closure();
}
}
}
namespaceExtensionDeclaration(
namespace: string | undefined,
closure: Function
) {
if (namespace) {
this.printNewlineIfNeeded();
this.printOnNewline(SwiftSource.raw`/// ${namespace} namespace`);
this.printOnNewline(swift`public extension ${namespace}`);
this.pushScope({ typeName: namespace });
this.withinBlock(closure);
this.popScope();
} else {
if (closure) {
closure();
}
}
}
classDeclaration(
{ className, modifiers, superClass, adoptedProtocols = [] }: Class,
closure: Function
) {
this.printNewlineIfNeeded();
this.printOnNewline(
(
wrap(swift``, new SwiftSource(_join(modifiers, " ")), swift` `) ||
swift``
).concat(swift`class ${className}`)
);
this.print(
wrap(
swift`: `,
join(
[
superClass !== undefined
? SwiftSource.identifier(superClass)
: undefined,
...adoptedProtocols.map(SwiftSource.identifier),
],
", "
)
)
);
this.pushScope({ typeName: className });
this.withinBlock(closure);
this.popScope();
}
/**
* Generates the declaration for a struct
*
* @param param0 The struct name, description, adoptedProtocols, and namespace to use to generate the struct
* @param outputIndividualFiles If this operation is being output as individual files, to help prevent
* redundant usages of the `public` modifier in enum extensions.
* @param closure The closure to execute which generates the body of the struct.
*/
structDeclaration(
{
structName,
description,
adoptedProtocols = [],
namespace = undefined,
}: Struct,
outputIndividualFiles: boolean,
closure: Function
) {
this.printNewlineIfNeeded();
this.comment(description);
const isRedundant =
adoptedProtocols.includes("GraphQLFragment") &&
!!namespace &&
outputIndividualFiles;
const modifier = new SwiftSource(isRedundant ? "" : "public ");
this.printOnNewline(swift`${modifier}struct ${structName}`);
this.print(
wrap(swift`: `, join(adoptedProtocols.map(SwiftSource.identifier), ", "))
);
this.pushScope({ typeName: structName });
this.withinBlock(closure);
this.popScope();
}
propertyDeclaration({ propertyName, typeName, description }: Property) {
this.comment(description);
this.printOnNewline(swift`public var ${propertyName}: ${typeName}`);
}
propertyDeclarations(properties: Property[]) {
if (!properties) return;
properties.forEach((property) => this.propertyDeclaration(property));
}
protocolDeclaration(
{ protocolName, adoptedProtocols }: Protocol,
closure: Function
) {
this.printNewlineIfNeeded();
this.printOnNewline(swift`public protocol ${protocolName}`);
this.print(
wrap(
swift`: `,
join(
adoptedProtocols !== undefined
? adoptedProtocols.map(SwiftSource.identifier)
: undefined,
", "
)
)
);
this.pushScope({ typeName: protocolName });
this.withinBlock(closure);
this.popScope();
}
protocolPropertyDeclaration({ propertyName, typeName }: Property) {
this.printOnNewline(swift`var ${propertyName}: ${typeName} { get }`);
}
protocolPropertyDeclarations(properties: Property[]) {
if (!properties) return;
properties.forEach((property) =>
this.protocolPropertyDeclaration(property)
);
}
}