15
import * as ts from "typescript"
16
import * as path from "path"
18
createAnyType, createContainerType, createEnumType, createNumberType, createReferenceType, createStringType, createTypedef,
19
createTypeParameterReference, createUndefinedType, createUnionType, getExtAttribute, IDLCallable, IDLCallback, IDLConstructor,
20
IDLEntry, IDLEnum, IDLEnumMember, IDLExtendedAttribute, IDLFunction, IDLInterface, IDLKind, IDLMethod, IDLModuleType, IDLParameter, IDLProperty, IDLType, IDLTypedef
23
asString, capitalize, getComment, getDeclarationsByNode, getExportedDeclarationNameByDecl, getExportedDeclarationNameByNode, identName, isCommonMethodOrSubclass, isNodePublic, isReadonly, isStatic, nameOrNullForIdl as nameOrUndefined, stringOrNone
25
import { GenericVisitor } from "./options"
26
import { PeerGeneratorConfig } from "./peer-generation/PeerGeneratorConfig"
27
import { OptionValues } from "commander"
29
const typeMapper = new Map<string, string>(
31
["null", "undefined"],
32
["void", "undefined"],
34
["Array", "sequence"],
35
["string", "DOMString"],
39
["\"auto\"", "string"]
43
export class CompileContext {
48
export class IDLVisitor implements GenericVisitor<IDLEntry[]> {
49
private output: IDLEntry[] = []
50
private currentScope: IDLEntry[] = []
51
scopes: IDLEntry[][] = []
52
globalScope: IDLMethod[] = []
55
this.scopes.push(this.currentScope)
56
this.currentScope = []
60
const result = this.currentScope
61
this.currentScope = this.scopes.pop()!
66
private sourceFile: ts.SourceFile,
67
private typeChecker: ts.TypeChecker,
68
private compileContext: CompileContext,
69
private options: OptionValues) { }
71
visitWholeFile(): IDLEntry[] {
72
ts.forEachChild(this.sourceFile, (node) => this.visit(node))
73
if (this.globalScope.length > 0) {
75
kind: IDLKind.Interface,
76
name: `GlobalScope_${path.basename(this.sourceFile.fileName).replace(".d.ts", "")}`,
77
extendedAttributes: [ {name: "GlobalScope" } ],
78
methods: this.globalScope,
89
visit(node: ts.Node) {
90
if (ts.isClassDeclaration(node)) {
91
this.output.push(this.serializeClass(node))
92
} else if (ts.isInterfaceDeclaration(node)) {
93
this.output.push(this.serializeInterface(node))
94
} else if (ts.isModuleDeclaration(node)) {
95
if (this.isKnownAmbientModuleDeclaration(node)) {
96
this.output.push(this.serializeAmbientModuleDeclaration(node))
99
ts.forEachChild(node, (node) => this.visit(node));
102
} else if (ts.isEnumDeclaration(node)) {
103
this.output.push(this.serializeEnum(node))
104
} else if (ts.isTypeAliasDeclaration(node)) {
105
this.output.push(this.serializeTypeAlias(node))
106
} else if (ts.isFunctionDeclaration(node)) {
107
this.globalScope.push(this.serializeMethod(node))
111
serializeAmbientModuleDeclaration(node: ts.ModuleDeclaration): IDLModuleType {
112
const name = nameOrUndefined(node.name) ?? "UNDEFINED_Module"
114
kind: IDLKind.ModuleType,
116
extendedAttributes: [ {name: "VerbatimDts", value: `"${escapeAmbientModuleContent(this.sourceFile, node)}"`}]
120
serializeTypeAlias(node: ts.TypeAliasDeclaration): IDLTypedef | IDLFunction | IDLInterface {
121
const name = nameOrUndefined(node.name) ?? "UNDEFINED_TYPE_NAME"
122
if (ts.isImportTypeNode(node.type)) {
123
let original = node.type.getText()
125
kind: IDLKind.Typedef,
127
extendedAttributes: [ { name: "VerbatimDts", value: `"${original}"` }],
128
type: createReferenceType(`Imported${name}`)
131
if (ts.isFunctionTypeNode(node.type)) {
132
return this.serializeFunctionType(name, node.type)
134
if (ts.isTypeLiteralNode(node.type)) {
135
return this.serializeObjectType(name, node.type)
137
return createTypedef(name, this.serializeType(node.type))
140
heritageIdentifiers(heritage: ts.HeritageClause): ts.Identifier[] {
141
return heritage.types.map(it => {
142
return ts.isIdentifier(it.expression) ? it.expression : undefined
143
}).filter(it => !!it) as ts.Identifier[]
146
baseDeclarations(heritage: ts.HeritageClause): ts.Declaration[] {
147
return this.heritageIdentifiers(heritage)
148
.map(it => getDeclarationsByNode(this.typeChecker, it)[0])
152
serializeHeritage(heritage: ts.HeritageClause): IDLType[] {
153
return heritage.types.map(it => {
155
(ts.isIdentifier(it.expression)) ?
156
ts.idText(it.expression) :
157
`NON_IDENTIFIER_HERITAGE ${asString(it)}`
158
return createReferenceType(name)
162
serializeInheritance(inheritance: ts.NodeArray<ts.HeritageClause> | undefined): IDLType[] {
163
return inheritance?.map(it => this.serializeHeritage(it)).flat() ?? []
166
computeExtendedAttributes(isClass: boolean, node: ts.ClassDeclaration | ts.InterfaceDeclaration): IDLExtendedAttribute[] | undefined {
167
let result: IDLExtendedAttribute[] = []
168
if (isClass) result.push({name: "Class"})
169
let name = identName(node.name)
170
if (name && ts.isClassDeclaration(node) && isCommonMethodOrSubclass(this.typeChecker, node)) {
171
result.push({name: "Component", value: PeerGeneratorConfig.mapComponentName(name)})
173
if (PeerGeneratorConfig.isKnownParametrized(name)) {
174
result.push({name: "Parametrized", value: "T"})
176
return result.length > 0 ? result : undefined
180
serializeClass(node: ts.ClassDeclaration): IDLInterface {
182
const result: IDLInterface = {
184
extendedAttributes: this.computeExtendedAttributes(true, node),
185
name: getExportedDeclarationNameByDecl(node) ?? "UNDEFINED",
186
documentation: getDocumentation(this.sourceFile, node, this.options.docs),
187
inheritance: this.serializeInheritance(node.heritageClauses),
188
constructors: node.members.filter(ts.isConstructorDeclaration).map(it => this.serializeConstructor(it as ts.ConstructorDeclaration)),
189
properties: this.pickProperties(node.members),
190
methods: this.pickMethods(node.members),
193
result.scope = this.endScope()
197
pickConstructors(members: ReadonlyArray<ts.TypeElement>): IDLConstructor[] {
198
return members.filter(ts.isConstructSignatureDeclaration)
199
.map(it => this.serializeConstructor(it as ts.ConstructSignatureDeclaration))
201
pickProperties(members: ReadonlyArray<ts.TypeElement | ts.ClassElement>): IDLProperty[] {
203
.filter(it => ts.isPropertySignature(it) || ts.isPropertyDeclaration(it) || this.isCommonMethodUsedAsProperty(it))
204
.map(it => this.serializeProperty(it))
206
pickMethods(members: ReadonlyArray<ts.TypeElement | ts.ClassElement>): IDLMethod[] {
208
.filter(it => (ts.isMethodSignature(it) || ts.isMethodDeclaration(it) || ts.isIndexSignatureDeclaration(it)) && !this.isCommonMethodUsedAsProperty(it))
209
.map(it => this.serializeMethod(it as ts.MethodDeclaration|ts.MethodSignature))
211
pickCallables(members: ReadonlyArray<ts.TypeElement>): IDLFunction[] {
212
return members.filter(ts.isCallSignatureDeclaration)
213
.map(it => this.serializeCallable(it))
216
fakeOverrides(node: ts.InterfaceDeclaration): ts.TypeElement[] {
217
return node.heritageClauses
218
?.flatMap(it => this.baseDeclarations(it))
219
?.flatMap(it => ts.isInterfaceDeclaration(it) ? it.members : [])
220
?.filter(it => !!it) ?? []
223
filterNotOverridden(overridden: Set<string>, node: ts.InterfaceDeclaration): ts.TypeElement[] {
224
return node.members.filter(it =>
225
it.name && ts.isIdentifier(it.name) && !overridden.has(ts.idText(it.name))
229
membersWithFakeOverrides(node: ts.InterfaceDeclaration): ts.TypeElement[] {
230
const result: ts.TypeElement[] = []
231
const worklist: ts.InterfaceDeclaration[] = [node]
232
const overridden = new Set<string>()
233
while (worklist.length != 0) {
234
const next = worklist.shift()!
235
const fakeOverrides = this.filterNotOverridden(overridden, next)
237
.map(it => nameOrUndefined(it.name))
238
.forEach(it => it ? overridden.add(it) : undefined)
239
result.push(...fakeOverrides)
240
const bases = next.heritageClauses
241
?.flatMap(it => this.baseDeclarations(it))
242
?.filter(it => ts.isInterfaceDeclaration(it)) as ts.InterfaceDeclaration[]
244
worklist.push(...bases)
250
serializeInterface(node: ts.InterfaceDeclaration): IDLInterface {
252
const allMembers = this.membersWithFakeOverrides(node)
253
const result: IDLInterface = {
254
kind: IDLKind.Interface,
255
name: getExportedDeclarationNameByDecl(node) ?? "UNDEFINED",
256
extendedAttributes: this.computeExtendedAttributes(false, node),
257
documentation: getDocumentation(this.sourceFile, node, this.options.docs),
258
inheritance: this.serializeInheritance(node.heritageClauses),
259
constructors: this.pickConstructors(node.members),
260
properties: this.pickProperties(allMembers),
261
methods: this.pickMethods(allMembers),
262
callables: this.pickCallables(node.members)
264
result.scope = this.endScope()
268
serializeObjectType(name: string, node: ts.TypeLiteralNode): IDLInterface {
270
kind: IDLKind.AnonymousInterface,
273
constructors: this.pickConstructors(node.members),
274
properties: this.pickProperties(node.members),
275
methods: this.pickMethods(node.members),
276
callables: this.pickCallables(node.members)
280
serializeEnum(node: ts.EnumDeclaration): IDLEnum {
283
name: ts.idText(node.name),
284
documentation: getDocumentation(this.sourceFile, node, this.options.docs),
285
elements: node.members.filter(ts.isEnumMember)
286
.map(it => this.serializeEnumMember(it))
290
serializeEnumMember(node: ts.EnumMember): IDLEnumMember {
292
let initializer: string|number|undefined = undefined
293
if (!node.initializer) {
295
} else if (ts.isStringLiteral(node.initializer)) {
297
initializer = node.initializer.text
298
} else if (ts.isNumericLiteral(node.initializer)) {
300
initializer = node.initializer.text
302
ts.isBinaryExpression(node.initializer) &&
303
node.initializer.operatorToken.kind == ts.SyntaxKind.LessThanLessThanToken &&
304
ts.isNumericLiteral(node.initializer.right) &&
305
ts.isNumericLiteral(node.initializer.left)
308
initializer = (+node.initializer.left.text) << (+node.initializer.right.text)
312
initializer = node.initializer.getText(this.sourceFile)
313
console.log("Unrepresentable enum initializer: ", initializer)
316
kind: IDLKind.EnumMember,
317
name: nameOrUndefined(node.name)!,
318
type: isString ? createStringType() : createNumberType(),
319
initializer: initializer
323
serializeFunctionType(name: string, signature: ts.SignatureDeclarationBase): IDLCallback {
325
kind: IDLKind.Callback,
327
parameters: signature.parameters.map(it => this.serializeParameter(it)),
328
returnType: this.serializeType(signature.type),
332
addToScope(callback: IDLEntry) {
333
this.currentScope.push(callback)
336
isTypeParameterReference(type: ts.TypeNode): boolean {
337
if (!ts.isTypeReferenceNode(type)) return false
338
const name = type.typeName
340
const declaration = getDeclarationsByNode(this.typeChecker, name)[0]
341
if (!declaration) return false
342
if (ts.isTypeParameterDeclaration(declaration)) return true
346
isKnownParametrizedType(type: ts.TypeNode): boolean {
347
if (!ts.isTypeReferenceNode(type)) return false
348
let parent = type.parent
349
while (parent && !ts.isClassDeclaration(parent) && !ts.isInterfaceDeclaration(parent)) {
350
parent = parent.parent
352
if (!parent) return false
353
const name = identName(parent.name)
354
return PeerGeneratorConfig.isKnownParametrized(name)
357
isKnownAmbientModuleDeclaration(type: ts.Node): boolean {
358
if (!ts.isModuleDeclaration(type)) return false
359
const name = identName(type)
360
const ambientModuleNames = this.typeChecker.getAmbientModules().map(it=>it.name.replaceAll('\"',""))
361
return name != undefined && ambientModuleNames.includes(name)
364
warn(message: string) {
365
console.log(`WARNING: ${message}`)
368
serializeType(type: ts.TypeNode | undefined, nameSuggestion: string|undefined = undefined): IDLType {
369
if (type == undefined) return createUndefinedType()
371
if (type.kind == ts.SyntaxKind.UndefinedKeyword ||
372
type.kind == ts.SyntaxKind.NullKeyword ||
373
type.kind == ts.SyntaxKind.VoidKeyword) {
374
return createUndefinedType()
376
if (type.kind == ts.SyntaxKind.Unknown) {
377
return createReferenceType("unknown")
380
if (type.kind == ts.SyntaxKind.NumberKeyword) {
381
return createNumberType()
383
if (type.kind == ts.SyntaxKind.StringKeyword) {
384
return createStringType()
386
if (ts.isUnionTypeNode(type)) {
387
return createUnionType(
388
type.types.map(it => this.serializeType(it))
391
if (this.isTypeParameterReference(type)) {
392
if (this.isTypeParameterReferenceOfCommonMethod(type) || this.isKnownParametrizedType(type)) {
393
return createReferenceType("this")
395
return createTypeParameterReference(nameOrUndefined((type as ts.TypeReferenceNode).typeName) ?? "UNEXPECTED_TYPE_PARAMETER")
397
if (ts.isTypeReferenceNode(type)) {
398
if (ts.isQualifiedName(type.typeName)) {
399
let left = type.typeName.left
400
let declaration = getDeclarationsByNode(this.typeChecker, left)
401
if (declaration.length > 0) {
402
if (ts.isEnumDeclaration(declaration[0])) {
403
return createEnumType(left.getText(left.getSourceFile()))
405
if (ts.isModuleDeclaration(declaration[0])) {
406
let rightName = type.typeName.right.getText(left.getSourceFile())
408
return createEnumType(`${rightName}`)
410
throw new Error(`Not supported for now: ${type.getText(this.sourceFile)}`)
413
let declaration = getDeclarationsByNode(this.typeChecker, type.typeName)
414
if (declaration.length == 0) {
415
let name = type.typeName.getText(type.typeName.getSourceFile())
416
this.warn(`Do not know type ${name}`)
417
return createReferenceType(name)
419
let isEnum = ts.isEnumDeclaration(declaration[0])
420
const rawType = sanitize(getExportedDeclarationNameByNode(this.typeChecker, type.typeName))!
421
const transformedType = typeMapper.get(rawType) ?? rawType
422
if (rawType == "AnimationRange") {
423
let typeArg = type.typeArguments![0]
424
return createReferenceType(`AnimationRange${capitalize(typeArg.getText(this.sourceFile))}`)
426
if (rawType == "Array" || rawType == "Promise" || rawType == "Map") {
427
return createContainerType(transformedType, type.typeArguments!.map(it => this.serializeType(it)))
429
return isEnum ? createEnumType(transformedType) : createReferenceType(transformedType)
431
if (ts.isArrayTypeNode(type)) {
432
return createContainerType("sequence", [this.serializeType(type.elementType)])
434
if (ts.isTupleTypeNode(type)) {
436
return createContainerType("sequence", [this.serializeType(type.elements[0])])
438
if (ts.isParenthesizedTypeNode(type)) {
439
return this.serializeType(type.type)
441
if (ts.isFunctionTypeNode(type)) {
442
const counter = this.compileContext.functionCounter++
443
const name = `${nameSuggestion??"callback"}__${counter}`
444
const callback = this.serializeFunctionType(name, type)
445
this.addToScope(callback)
446
return createReferenceType(name)
448
if (ts.isIndexedAccessTypeNode(type)) {
450
return createStringType()
452
if (ts.isTypeLiteralNode(type)) {
453
const counter = this.compileContext.objectCounter++
454
const name = `${nameSuggestion ?? "anonymous_interface"}__${counter}`
455
const literal = this.serializeObjectType(name, type)
456
this.addToScope(literal)
457
return createReferenceType(name)
459
if (ts.isLiteralTypeNode(type)) {
460
const literal = type.literal
461
if (ts.isStringLiteral(literal) || ts.isNoSubstitutionTemplateLiteral(literal) || ts.isRegularExpressionLiteral(literal)) {
462
return createStringType()
464
if (ts.isNumericLiteral(literal)) {
465
return createNumberType()
467
if (literal.kind == ts.SyntaxKind.NullKeyword) {
469
return createUndefinedType()
471
throw new Error(`Non-representable type: ${asString(type)}`)
472
return createAnyType("/* Non-representable literal type */ ")
474
if (ts.isTemplateLiteralTypeNode(type)) {
475
return createStringType()
477
if (ts.isImportTypeNode(type)) {
478
let originalText = `${type.getText(this.sourceFile)}`
479
this.warn(`import type: ${originalText}`)
480
let where = type.argument.getText(type.getSourceFile()).split("/").map(it => it.replaceAll("'", ""))
481
let what = asString(type.qualifier)
482
let typeName = `/* ${type.getText(this.sourceFile)} */ ` + sanitize(what == "default" ? "Imported" + where[where.length - 1] : "Imported" + what)
483
let result = createReferenceType(typeName)
484
result.extendedAttributes = [{ name: "Import", value: originalText}]
487
if (ts.isNamedTupleMember(type)) {
488
return this.serializeType(type.type)
493
let rawType = type.getText(this.sourceFile)
494
const transformedType = typeMapper.get(rawType) ?? rawType
495
return createReferenceType(transformedType)
498
isTypeParameterReferenceOfCommonMethod(type: ts.TypeNode): boolean {
499
if (!ts.isTypeReferenceNode(type)) return false
500
const name = type.typeName
501
const declaration = getDeclarationsByNode(this.typeChecker, name)[0]
502
if (!declaration) return false
503
if (ts.isTypeParameterDeclaration(declaration)) {
504
let parent = declaration.parent
505
if (ts.isClassDeclaration(parent)) {
506
return isCommonMethodOrSubclass(this.typeChecker, parent)
513
deduceFromComputedProperty(name: ts.PropertyName): string | undefined {
514
if (!ts.isComputedPropertyName(name)) return undefined
515
const expression = name.expression
516
if (!ts.isPropertyAccessExpression(expression)) return undefined
517
const receiver = expression.expression
518
if (!ts.isIdentifier(receiver)) return undefined
519
const field = expression.name
520
if (!ts.isIdentifier(field)) return undefined
522
const enumDeclaration = getDeclarationsByNode(this.typeChecker, receiver)[0]
523
if (!enumDeclaration || !ts.isEnumDeclaration(enumDeclaration)) return undefined
524
const enumMember = getDeclarationsByNode(this.typeChecker, field)[0]
525
if (!enumMember || !ts.isEnumMember(enumMember)) return undefined
526
const initializer = enumMember.initializer
527
if (!initializer || !ts.isStringLiteral(initializer)) return undefined
529
return initializer.text
532
propertyName(name: ts.PropertyName): string | undefined {
533
return this.deduceFromComputedProperty(name) ?? nameOrUndefined(name)
536
serializeProperty(property: ts.TypeElement | ts.ClassElement): IDLProperty {
537
if (ts.isMethodDeclaration(property) || ts.isMethodSignature(property)) {
538
const name = asString(property.name)
539
if (!this.isCommonMethodUsedAsProperty(property)) throw new Error("Wrong")
541
kind: IDLKind.Property,
543
extendedAttributes: [{ name: "CommonMethod" } ],
544
documentation: getDocumentation(this.sourceFile, property, this.options.docs),
545
type: this.serializeType(property.parameters[0].type),
552
if (ts.isPropertyDeclaration(property) || ts.isPropertySignature(property)) {
553
const name = this.propertyName(property.name)
555
kind: IDLKind.Property,
557
documentation: getDocumentation(this.sourceFile, property, this.options.docs),
558
type: this.serializeType(property.type, name),
559
isReadonly: isReadonly(property.modifiers),
560
isStatic: isStatic(property.modifiers),
561
isOptional: !!property.questionToken,
562
extendedAttributes: !!property.questionToken ? [{name: 'Optional'}] : undefined,
565
throw new Error("Unknown")
568
serializeParameter(parameter: ts.ParameterDeclaration): IDLParameter {
569
const name = nameOrUndefined(parameter.name)
571
kind: IDLKind.Parameter,
572
name: name ?? "Unexpected property name",
573
type: this.serializeType(parameter.type, name),
574
isVariadic: !!parameter.dotDotDotToken,
575
isOptional: !!parameter.questionToken
579
isCommonAttributeMethod(method: ts.MethodDeclaration|ts.MethodSignature): boolean {
580
let parent = method.parent
581
if (ts.isClassDeclaration(parent)) {
582
return isCommonMethodOrSubclass(this.typeChecker, parent)
587
isCommonMethodUsedAsProperty(member: ts.ClassElement | ts.TypeElement): member is (ts.MethodDeclaration | ts.MethodSignature) {
588
return (this.options.commonToAttributes ?? true) &&
589
(ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) &&
590
this.isCommonAttributeMethod(member) &&
591
member.parameters.length == 1
595
serializeMethod(method: ts.MethodDeclaration | ts.MethodSignature | ts.IndexSignatureDeclaration | ts.FunctionDeclaration): IDLMethod {
596
if (ts.isIndexSignatureDeclaration(method)) {
598
kind: IDLKind.Method,
599
name: "indexSignature",
600
documentation: getDocumentation(this.sourceFile, method, this.options.docs),
601
returnType: this.serializeType(method.type),
602
extendedAttributes: [{name: 'IndexSignature' }],
604
parameters: method.parameters.map(it => this.serializeParameter(it))
607
let [methodName, escapedName] = escapeMethodName(method.name!.getText(this.sourceFile))
609
kind: IDLKind.Method,
611
extendedAttributes: (methodName != escapedName) ? [ { name: "DtsName", value: `"${methodName}"`} ] : undefined,
612
documentation: getDocumentation(this.sourceFile, method, this.options.docs),
613
parameters: method.parameters.map(it => this.serializeParameter(it)),
614
returnType: this.serializeType(method.type),
615
isStatic: isStatic(method.modifiers)
619
serializeCallable(method: ts.CallSignatureDeclaration): IDLCallable {
621
kind: IDLKind.Callable,
623
extendedAttributes: [{name: "CallSignature"}],
624
documentation: getDocumentation(this.sourceFile, method, this.options.docs),
625
parameters: method.parameters.map(it => this.serializeParameter(it)),
626
returnType: this.serializeType(method.type),
631
serializeConstructor(constr: ts.ConstructorDeclaration|ts.ConstructSignatureDeclaration): IDLConstructor {
632
constr.parameters.forEach(it => {
633
if (isNodePublic(it)) console.log("TODO: count public/private/protected constructor args as properties")
637
kind: IDLKind.Constructor,
639
parameters: constr.parameters.map(it => this.serializeParameter(it)),
640
returnType: this.serializeType(constr.type),
645
function sanitize(type: stringOrNone): stringOrNone {
646
if (!type) return undefined
647
let dotIndex = type.lastIndexOf(".")
649
return type.substring(dotIndex + 1)
655
function escapeMethodName(name: string) : [string, string] {
656
if (name.startsWith("$")) return [name, name.replace("$", "dollar_")]
660
function escapeAmbientModuleContent(sourceFile: ts.SourceFile, node: ts.Node) : string {
661
const { pos, end} = node
662
const content = sourceFile.text.substring(pos,end)
663
return content.replaceAll('"', "'")
666
function getDocumentation(sourceFile: ts.SourceFile, node: ts.Node, docsOption: string): string | undefined {
667
switch (docsOption) {
668
case 'all': return getComment(sourceFile, node)
669
case 'opt': return dedupDocumentation(getComment(sourceFile, node))
670
case 'none': return undefined
671
default: throw new Error(`Unknown option docs=${docsOption}`)
675
function dedupDocumentation(documentation: string): string {
676
let seen: Set<string> = new Set()
677
let firstLine = false
682
if (t.startsWith('/*')) {
686
if (t == '' || t === '*') {
690
if (t.startsWith('*/')) return true