-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathConverterContext.hx
2129 lines (1862 loc) · 87.2 KB
/
ConverterContext.hx
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
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import ds.OnlyOnceSymbolQueue;
import ds.Set;
import haxe.macro.Expr;
import tool.FileTools;
import tool.TsSyntaxTools;
import typescript.Ts;
import typescript.ts.CompilerHost;
import typescript.ts.CompilerOptions;
import typescript.ts.Declaration;
import typescript.ts.GenericType;
import typescript.ts.InterfaceType;
import typescript.ts.IntersectionType;
import typescript.ts.Modifier;
import typescript.ts.ModifiersArray;
import typescript.ts.Node;
import typescript.ts.NodeBuilderFlags;
import typescript.ts.NumberLiteralType;
import typescript.ts.ObjectFlags;
import typescript.ts.ObjectType;
import typescript.ts.PackageId;
import typescript.ts.ParameterDeclaration;
import typescript.ts.Program;
import typescript.ts.ResolvedModuleFull;
import typescript.ts.Signature;
import typescript.ts.Symbol;
import typescript.ts.SymbolFlags;
import typescript.ts.SyntaxKind;
import typescript.ts.TupleType;
import typescript.ts.TupleTypeReference;
import typescript.ts.TypeAliasDeclaration;
import typescript.ts.TypeChecker;
import typescript.ts.TypeFlags;
import typescript.ts.TypeFormatFlags;
import typescript.ts.TypeNode;
import typescript.ts.TypeParameter;
import typescript.ts.TypeParameterDeclaration;
import typescript.ts.TypeReference;
import typescript.ts.UnionType;
using Lambda;
using StringTools;
using SupportTypes;
using TsInternal;
using tool.HaxeTools;
using tool.SymbolAccessTools;
using tool.TsProgramTools;
using tool.TsSymbolTools;
using tool.TsTypeTools;
private typedef TsType = typescript.ts.Type;
private typedef Options = {
locationComments: Bool,
allowIntersectionRasterization: Bool,
/**
When true, symbols defined externally can be included in the output module.
This is a workaround until we have a single compilation context for all dependencies
**/
queueExternalSymbols: Bool,
enableTypeParameterConstraints: Bool,
globalPackageName: Null<String>,
globalTypes: Bool,
modularTypes: Bool,
}
@:expose
@:nullSafety
class ConverterContext {
/**
Normalized input module id (without prefix @types/).
This is the value to use in `require()` to load the module at runtime.
It may be a path, for example `./modules/example`
**/
public final normalizedInputModuleName: String;
public final inputModule: ResolvedModuleFull;
/**
If the inputModule is part of a node package, this will be set to the packageId.
For example, in three.js, there are a number of types that are not accessible from `require('three')`:
If inputModule is `three/examples/jsm/controls/OrbitControls`, packageId will be `three`
**/
public final packageName: Null<String>;
/**
Map of package-paths to HaxeModules
**/
public final generatedModules = new Map<String, HaxeModule>();
/**
An array of normalized module ids (paths or names) that this module depends on.
These dependencies will also need to be converted
**/
public final moduleDependencies: haxe.ds.ReadOnlyArray<{ normalizedModuleName: String, packageInfo: PackageId }>;
public final tc: TypeChecker;
public final host: CompilerHost;
public final program: Program;
public final moduleSearchPath: String;
final options: Options;
/**
Symbol access map is filled during an initial pass over the program.
The access path for key-symbols such as types, are stored so we can retrieve them later when we have a type-reference
**/
public final symbolAccessMap: SymbolAccessMap;
/**
Using the symbol access map, a type-path is generated for every symbol in the program (including symbols in other modules and in the lib files).
This is done upfront to ensure deterministic handling of module name deduplication
If getting a reference to a symbol, use `getReferencedHaxeTypePath` instead
**/
final haxeTypePathMap: HaxeTypePathMap;
/**
Unique list of symbols to convert
When a type is referenced during conversion, if it is inaccessible (and therefore not converted in the first pass), add it to this queue to be converted
**/
final declarationSymbolQueue = new OnceOnlySymbolQueue();
/**
To aid post processing, when a symbol is popped `declarationSymbolQueue` after converting is it added to this list
**/
final processedDeclarationSymbols = new Array<Symbol>();
// settings
final shortenTypePaths = true;
final typeStackLimit = 25;
final anyUnionCollapse = false; // `any | string` -> `any`
final unionizedFunctionTypes = true; // `(?b) => C` -> `()->C | (b)->C`
final hxnodejsMap: Null<typemap.TypeMap>;
public function new(
inputModuleName: String,
moduleSearchPath: String,
compilerOptions: CompilerOptions,
stdLibMap: Null<typemap.TypeMap>,
hxnodejsMap: Null<typemap.TypeMap>,
options: Options
) {
this.options = options;
this.hxnodejsMap = hxnodejsMap;
// we make the moduleSearchPath absolute to work around an issue in resolveModuleName
moduleSearchPath = sys.FileSystem.absolutePath(moduleSearchPath);
this.moduleSearchPath = moduleSearchPath;
this.host = Ts.createCompilerHost(compilerOptions);
// this will be used as the argument to require()
this.normalizedInputModuleName = inline inputModuleName.normalizeModuleName();
Console.log('Converting module <b>$inputModuleName</b>');
Log.log('moduleSearchPath: <b>"${this.moduleSearchPath}"</>');
// resolve input module (as entry-point)
var result = Ts.resolveModuleName(inputModuleName, moduleSearchPath + '/.', compilerOptions, host);
if (result.resolvedModule == null) {
var failedLookupLocations: Array<String> = Reflect.field(result, 'failedLookupLocations'); // @internal field
Log.error('Failed to find typescript for module <b>"${inputModuleName}"</b>. Searched the following paths:<dim>\n\t${failedLookupLocations.join('\n\t')}</>');
throw 'Input module not resolved';
}
inputModule = result.resolvedModule;
// if the input module is part of a package, resolve the package
var packageRootModule = if (inputModule.packageId != null) {
this.packageName = inline inputModule.packageId.name.normalizeModuleName();
if (inputModule.packageId.name != inputModuleName) {
var result = Ts.resolveModuleName(this.packageName, moduleSearchPath + '/.', compilerOptions, host);
if (result.resolvedModule == null) {
var failedLookupLocations: Array<String> = Reflect.field(result, 'failedLookupLocations'); // @internal field
Log.error('Root package for <b>$inputModuleName</> was <b>${this.packageName}</> but this module could not be resolved. Searched the following paths:<dim>\n\t${failedLookupLocations.join('\n\t')}</>');
}
result.resolvedModule;
} else {
inputModule;
}
} else null;
// create program
var inputSourcePaths = [inputModule.resolvedFileName];
if (packageRootModule != null) {
inputSourcePaths.unshift(packageRootModule.resolvedFileName);
}
this.program = Ts.createProgram(inputSourcePaths, compilerOptions, host);
this.tc = program.getTypeChecker();
Log.diagnostics(program.getAllDiagnostics());
var entryPointSourceFile = program.getSourceFile(inputModule.resolvedFileName);
if (entryPointSourceFile == null) throw 'Types not found – try installing external types with:\n\t<bg_black,white>npm install @types/$inputModuleName</>';
// these are source files that belong directly to this module
// @! maybe we could search the package directory and add all discovered typescript files here
var inputModuleSourceFiles = new Array();
program.walkReferencedSourceFiles(entryPointSourceFile, host, true, s -> inputModuleSourceFiles.push(s));
// by default, the .moduleName field of source files is not assigned
// this method explores each source file and gives it a .moduleName field that would (if the source file exports symbols) be used in require()
program.assignModuleNames(moduleSearchPath, host);
// determine external dependencies:
var dependencies = program.getDependencies(inputModuleSourceFiles, normalizedInputModuleName, host);
// skip node types dependency if we are using hxnodejs
if (hxnodejsMap != null) {
dependencies = dependencies.filter(d -> d.normalizedModuleName != 'node');
}
moduleDependencies = dependencies;
// populate symbol access map
symbolAccessMap = new SymbolAccessMap(program);
// generate a haxe type-path for all type or module-class (ValueModule) symbols in the program
haxeTypePathMap = new HaxeTypePathMap(
packageName != null ? packageName : normalizedInputModuleName,
options.globalPackageName,
program,
symbolAccessMap,
stdLibMap,
hxnodejsMap
);
// convert symbols, starting from entry-point file
program.walkReferencedSourceFiles(entryPointSourceFile, host, true, (sourceFile) -> {
for (symbol in program.getExposedSymbolsOfSourceFile(sourceFile)) {
TsSymbolTools.walkDeclarationSymbols(tc, symbol, (symbol, access) -> {
declarationSymbolQueue.tryEnqueue(symbol);
});
}
});
// convert declaration symbols (types and module variables)
// declarationSymbolQueue grows as types are referenced during conversion
while (!declarationSymbolQueue.empty()) {
var symbol: Symbol = cast declarationSymbolQueue.dequeue();
for (access in symbolAccessMap.getAccess(symbol)) {
// Log.log(access.toString(), symbol);
// symbol can be both a haxe-module source and a global field if it has multiple declarations
if (isHaxeModuleSource(tc, symbol, access)) {
generateHaxeModulesFromSymbol(symbol, access);
}
if (isGlobalField(tc, symbol, access)) {
var globalModule = this.getGlobalModuleForFieldSymbol(symbol, access);
var field = fieldFromSymbol(symbol.name, symbol, symbol, Global([]), null);
field.enableAccess(AStatic);
globalModule.fields.push(field);
}
// we will also get module variable symbols here but these are handled in `generateHaxeModulesFromSymbol` instead
}
processedDeclarationSymbols.push(symbol);
}
// special case pattern detection and field name clash resolution
PostProcess.run(this);
}
/**
Returns true if the symbol corresponds to a module in haxe
**/
static public function isHaxeModuleSource(tc: TypeChecker, symbol: Symbol, access: SymbolAccess) {
return
symbol.flags & (SymbolFlags.Type | SymbolFlags.ValueModule) != 0 ||
tc.isConstructorTypeVariableSymbol(symbol) ||
(symbol.flags & (SymbolFlags.Function | SymbolFlags.Variable) != 0 && access.match(ExportModule(_, _, [])));
}
/**
Some symbols are not natively supported in haxe, for example, variables outside classes
These symbols are implemented as classes in haxe
**/
static public function requiresHxClass(tc: TypeChecker, symbol: Symbol) {
return (
symbol.flags & (SymbolFlags.Class | SymbolFlags.ValueModule | SymbolFlags.Value) != 0 ||
tc.isConstructorTypeVariableSymbol(symbol)
);
}
/**
In TypeScript, a single named type (symbol) can have multiple definitions, for example, as a class and an interface.
Because the haxe type-system is more restrictive, there are contexts where we want the interface version of a type, and others when we want the class version.
For example, we want the class version in `class extends`, and the interface-structure for intersection
**/
static public function requiresAdditionalStructureType(tc: TypeChecker, symbol: Symbol) {
// we need an additional interface-structure type if the symbol is declared as an interface and as a class-like type
return (symbol.flags & SymbolFlags.Interface != 0) && requiresHxClass(tc, symbol);
}
static public function isGlobalField(tc: TypeChecker, symbol: Symbol, access: SymbolAccess): Bool {
return switch access {
case Global([_])
if (
symbol.flags & (SymbolFlags.Variable | SymbolFlags.Function) != 0 &&
!tc.isConstructorTypeVariableSymbol(symbol)
): true;
default: false;
}
}
/**
Returns a TypePath for a given symbol.
The symbol must have flags Type or ValueModule.
This also queues this symbol's type to be converted if it isn't already
**/
public function getReferencedHaxeTypePath(symbol: Symbol, moduleSymbol: Symbol, accessContext: SymbolAccess, preferInterfaceStructure: Bool): TypePath {
var hxTypePath = haxeTypePathMap.getTypePath(symbol, accessContext, preferInterfaceStructure);
// should we queue this symbol for conversion?
if (!hxTypePath.isExistingStdLibType) {
if (symbol.getDeclarationsArray().exists(d -> d.getSourceFile().hasNoDefaultLib)) {
declarationSymbolQueue.tryEnqueue(symbol);
} else {
if (!declarationSymbolQueue.has(symbol)) {
/**
Here we're referencing a symbol that's not currently included in the conversion queue
this might be:
- A symbol of an external library (in which case, we don't need to convert it)
- An undiscovered symbol declared within the input module. I'm undecided on whether or not this is an error yet.
**/
// check if symbol is declared within this module
var declaredInModules = symbol.getParentModuleNames();
var declaredWithinInputModule = declaredInModules.exists(name -> name == normalizedInputModuleName);
if (declaredWithinInputModule) {
Log.log('Discovered symbol through reference', symbol);
declarationSymbolQueue.tryEnqueue(symbol);
} else if (options.queueExternalSymbols) {
Log.log('Queuing external symbol', symbol);
declarationSymbolQueue.tryEnqueue(symbol);
} else if (hxnodejsMap != null) {
// when using hxnodejs we want to generate any node types that couldn't be matched
if (hxTypePath.pack.join('.').startsWith('js.node.')) {
declarationSymbolQueue.tryEnqueue(symbol);
}
}
}
}
}
// if accessContext symbol has the same package as the target symbol, we can shorten the type path by removing the pack
// we don't shorten std lib types because they are not generated
var noPack = if (shortenTypePaths && !hxTypePath.isExistingStdLibType) {
if (moduleSymbol != null) {
var contextTypePath = haxeTypePathMap.getTypePath(moduleSymbol, accessContext, false);
contextTypePath.pack.join('.') == hxTypePath.pack.join('.'); // same package context
} else false;
} else false;
return {
name: hxTypePath.moduleName,
sub: hxTypePath.moduleName != hxTypePath.name ? hxTypePath.name : null,
pack: noPack ? [] : hxTypePath.pack,
}
}
public function getGeneratedModule(typePath: TypePath) {
return generatedModules.get(getHaxeModuleKey(typePath.pack, typePath.name));
}
/**
Symbol+access must be a haxe-module source symbol (see `isHaxeModuleSource()`)
**/
function generateHaxeModulesFromSymbol(symbol: Symbol, access: SymbolAccess) {
// Log.log('generateHaxeModulesFromSymbol() <yellow>${access.toString()}</>', symbol);
var pos = TsSymbolTools.getPosition(symbol);
var isConstructorTypeVariable = tc.isConstructorTypeVariableSymbol(symbol);
var isValueModuleOnlySymbol = symbol.flags & SymbolFlags.ValueModule != 0 && symbol.flags & SymbolFlags.Type == 0 && !isConstructorTypeVariable; // (allowed to be a variable symbol)
// the fundamental module of a symbol is the main representation of it in haxe
// some symbols haxe _two_ representations in haxe, for example a class + interface symbol will have a class and interface structure in haxe
var fundamentalTypePath = haxeTypePathMap.getTypePath(symbol, access, false);
// do not generate a module for std-lib types (some std-lib types might have an interface-structure however)
if (!fundamentalTypePath.isExistingStdLibType) {
var hxModule: HaxeModule = if (symbol.flags & SymbolFlags.Enum != 0) {
// a ConstEnum does not exist at runtime
var isCompileTimeEnum = symbol.flags & SymbolFlags.ConstEnum != 0;
var hxEnumType = complexTypeBaseOfEnumSymbol(symbol);
var enumMembers = tc.getExportsOfModule(symbol).filter(s -> s.flags & SymbolFlags.EnumMember != 0);
var hxEnumFields = [for (enumMember in enumMembers) fieldFromSymbol(enumMember.name, enumMember, symbol, access, null)];
{
pack: fundamentalTypePath.pack,
name: fundamentalTypePath.name,
kind: TDAbstract(hxEnumType, [hxEnumType], [hxEnumType]),
params: [],
isExtern: true,
fields: hxEnumFields,
doc: getDoc(symbol),
meta: (isCompileTimeEnum ? [] : [access.toAccessMetadata()]).concat([{name: ':enum', pos: pos}]),
pos: pos,
tsSymbol: symbol,
tsSymbolAccess: access,
}
} else if (symbol.flags & SymbolFlags.TypeAlias != 0) {
var typeAliasDeclaration: Null<TypeAliasDeclaration> = cast symbol.getDeclarationsArray().filter(node -> node.kind == SyntaxKind.TypeAliasDeclaration)[0];
if (typeAliasDeclaration == null) {
Log.warn('TypeAlias symbol did not have a TypeAliasDeclaration', symbol);
}
var tsType = tc.getDeclaredTypeOfSymbol(symbol);
var hxAliasType = complexTypeFromTsType(tsType, symbol, access, typeAliasDeclaration, symbol, false);
var forceAbstractKind = symbol.flags & SymbolFlags.ValueModule != 0 || isConstructorTypeVariable;
// if this symbol is also a ValueModule then it needs to have fields
// to enable this, we create a pseudo typedef with an abstract
var hxTypeDef: HaxeModule = if (forceAbstractKind) {
pack: fundamentalTypePath.pack,
name: fundamentalTypePath.name,
fields: [],
kind: TDAbstract(hxAliasType, [hxAliasType], [hxAliasType]),
params: typeParamDeclFromTypeDeclarationSymbol(symbol, access, typeAliasDeclaration), // is there a case where an enum can have a TypeParameter?
doc: getDoc(symbol),
isExtern: true,
meta: [access.toAccessMetadata(), {name: ':forward', pos: pos}, {name: ':forwardStatics', pos: pos}],
pos: pos,
tsSymbol: symbol,
tsSymbolAccess: access,
} else {
pack: fundamentalTypePath.pack,
name: fundamentalTypePath.name,
fields: [],
kind: TDAlias(hxAliasType),
params: typeParamDeclFromTypeDeclarationSymbol(symbol, access, typeAliasDeclaration),
doc: getDoc(symbol),
pos: pos,
tsSymbol: symbol,
tsSymbolAccess: access,
}
hxTypeDef;
} else if (requiresHxClass(tc, symbol)) {
// Class | ValueModule | ConstructorTypeVariable
// (null if not a class symbol)
// if class + function, symbol.valueDeclaration is the function declaration
var classDeclaration = symbol.declarations.find(d -> d.kind == SyntaxKind.ClassDeclaration);
var declaration = if (classDeclaration != null) {
classDeclaration;
} else if (symbol.valueDeclaration != null) {
symbol.valueDeclaration;
} else {
Log.error('Expected valueDeclaration for a symbol that requires a class in haxe', symbol);
null;
}
var declaredType = tc.getDeclaredTypeOfSymbol(symbol);
var meta = [access.toAccessMetadata()];
var superClassPath: Null<TypePath> = null;
if (isValueModuleOnlySymbol) {
meta.push({name: 'valueModuleOnly', pos: pos});
}
var callSignatures = tc.getSignaturesOfType(declaredType, Call);
var indexSignatures = tc.getIndexSignaturesOfType(declaredType);
var classMembers = tc.getPropertiesOfType(declaredType).filter(s -> s.isAccessibleField());
var classSuperType = tc.getClassExtendsType(symbol);
if (classSuperType != null) {
var hxSuperType = complexTypeFromObjectType(cast classSuperType, symbol, access, false, declaration);
superClassPath = switch hxSuperType {
case TPath(p) if (!isHxAny(hxSuperType)): p;
default:
Log.warn('Class super-type did not translate to a class-path (instead it was: <b>${new Printer().printComplexType(hxSuperType)}</>)', symbol);
null;
}
// remove redefined class **variable** fields (function redefinitions are allowed in haxe)
var classSuperMembers = tc.getPropertiesOfType(classSuperType).filter(s -> s.isAccessibleField());
classMembers = classMembers.filter(m -> {
var classSuperMatch = classSuperMembers.find(sm -> sm.name == m.name);
return if (classSuperMatch != null) {
if (m.flags & SymbolFlags.Method != 0) {
// methods _can_ be defined, although we should only redefine if the type changed
// compare types by comparing strings
var format: TypeFormatFlags = TypeFormatFlags.NoTruncation;
tc.typeToString(getTsTypeOfField(m), m.valueDeclaration, format) != tc.typeToString(getTsTypeOfField(classSuperMatch), classSuperMatch.valueDeclaration, format);
} else {
// variable fields cannot be redefined in haxe
false;
}
} else {
// field is not redefined from super
true;
}
});
}
var fields = generateTypeFields(
symbol,
access,
declaration,
symbol.getConstructorSignatures(tc),
callSignatures,
indexSignatures,
classMembers,
fundamentalTypePath.name
);
if (classDeclaration != null) {
// add default constructor
if (!fields.exists(f -> f.name == 'new') && classSuperType == null) {
fields.unshift((macro class { function new(); }).fields[0]);
}
}
// although we do a final pass resolving name collisions, we do it manually here so that if the fields are cloned into an interface structure
// they have the same collision resolution applied
fields.resolveNameCollisions();
{
pack: fundamentalTypePath.pack,
name: fundamentalTypePath.name,
fields: fields,
kind: TDClass(superClassPath, [], false, false),
params: typeParamDeclFromTypeDeclarationSymbol(symbol, access, declaration),
isExtern: true,
doc: getDoc(symbol),
meta: meta,
pos: pos,
tsSymbol: symbol,
tsSymbolAccess: access,
}
} else if (symbol.flags & SymbolFlags.Interface != 0) {
// interface-only symbol
createInterfaceModule(symbol, access, false);
} else {
Log.error('generateHaxeModulesFromSymbol(): Unhandled symbol, no flags were recognized', symbol);
{
pack: fundamentalTypePath.pack,
name: fundamentalTypePath.name,
fields: [],
kind: TDAbstract(macro :Dynamic, [macro :Dynamic], [macro :Dynamic]),
doc: getDoc(symbol),
isExtern: true,
pos: pos,
tsSymbol: symbol,
tsSymbolAccess: access,
}
}
/**
**Add ConstructType fields**
ConstructorType + Interface
-> Separate interface-structure, use class for constructor type, add static fields and new
ConstructorType + Type-Alias
-> Convert to abstract, add static fields and new
ConstructorType + ValueModule
-> add static fields and new
ConstructorType + Class => Not allowed
ConstructorType + Enum => Not allowed
**/
if (isConstructorTypeVariable) {
var constructorTypeDeclaration = symbol.valueDeclaration;
if (constructorTypeDeclaration != null) {
var constructorType = tc.getTypeOfSymbolAtLocation(symbol, constructorTypeDeclaration);
var constructSignatures = tc.getSignaturesOfType(constructorType, Construct);
var callSignatures = tc.getSignaturesOfType(constructorType, Call);
var indexSignatures = tc.getIndexSignaturesOfType(constructorType);
var fields = tc.getPropertiesOfType(constructorType).filter(s -> s.isAccessibleField());
if (indexSignatures.length > 0) {
Log.warn('Index signatures are not yet supported', symbol);
}
// given constructor type cannot merge with class, we don't expect an existing new signature
// (and constructor type new signature takes precedence, not overload, over a class new signature if merged with a type-alias to class)
var newField = newFieldFromSignatures(constructSignatures, symbol, access, constructorTypeDeclaration);
hxModule.fields.unshift(newField);
if (callSignatures.length > 0) {
var callField = functionFieldFromCallSignatures(selfCallFunctionName, callSignatures, symbol, access, constructorTypeDeclaration);
callField.enableAccess(AStatic);
hxModule.fields.push(callField);
}
// constructor type fields become class statics
for (field in fields) {
var hxField = fieldFromSymbol(field.name, field, symbol, access, constructorTypeDeclaration);
hxField.enableAccess(AStatic);
hxModule.fields.push(hxField);
}
} else {
Log.error('A symbol with a constructor type variable declaration should have a valueDeclaration', symbol);
}
}
// add static class members
for (staticClassMember in symbol.getExports().filter(s -> s.flags & SymbolFlags.ClassMember != 0 && s.isAccessibleField())) {
var field = fieldFromSymbol(staticClassMember.name, staticClassMember, symbol, access, null);
field.enableAccess(AStatic);
hxModule.fields.push(field);
}
// add module fields
if (symbol.flags & SymbolFlags.Module != 0) {
var moduleMemberFields = tc.getExportsOfModule(symbol).filter(s ->
s.flags & SymbolFlags.ModuleMember != 0 && (s.isAccessibleField() || s.flags & SymbolFlags.Alias != 0)
);
for (moduleMember in moduleMemberFields) {
// field name before alias resolution (e.g. `default`)
var nativeFieldName = moduleMember.name;
if (moduleMember.flags & SymbolFlags.Alias != 0) {
moduleMember = tc.getAliasedSymbol(moduleMember);
if (!moduleMember.isAccessibleField()) continue;
}
// skip constructor type variables because these have been converted into classes
if (tc.isConstructorTypeVariableSymbol(moduleMember)) continue;
var field = fieldFromSymbol(nativeFieldName, moduleMember, symbol, access, null);
field.enableAccess(AStatic);
hxModule.fields.push(field);
}
}
saveHaxeModule(hxModule);
}
// add special interface-structure module for the symbol if required
if (requiresAdditionalStructureType(tc, symbol)) {
saveHaxeModule(createInterfaceModule(symbol, access, true));
}
}
function createInterfaceModule(symbol: Symbol, access: SymbolAccess, preferInterfaceStructure: Bool): HaxeModule {
var pos = TsSymbolTools.getPosition(symbol);
var typePath = haxeTypePathMap.getTypePath(symbol, access, preferInterfaceStructure);
// there can be _multiple_ interface declarations, we just use the first
// the declaration is used to help guide type conversion, but it's not critical
var declaration = symbol.declarations.find(d -> d.kind == SyntaxKind.InterfaceDeclaration);
var declaredType = tc.getDeclaredTypeOfSymbol(symbol);
var declaredMembers = tc.getPropertiesOfType(declaredType).filter(s -> s.isAccessibleField());
var callSignatures = tc.getSignaturesOfType(declaredType, Call);
var kind = if (callSignatures.length > 0 && declaredMembers.length == 0 && preferInterfaceStructure == false) {
// handle special case of function type { (args): T }
// or if multiple signatures: { (args): T; (args2): T2 }, return a union of function types
var functionSignature = SupportTypes.getUnionType(this, callSignatures.map(callSignature -> complexTypeFromCallSignature(callSignature, symbol, access, declaration)));
TDAlias(functionSignature);
} else {
/*
// replace redefined class **variable** members with their super-type members
var classSuperType = tc.getClassExtendsType(symbol);
if (classSuperType != null) {
var classSuperMembers = tc.getPropertiesOfType(classSuperType).filter(s -> s.isAccessibleField());
// when extending a class, because we cannot redefine fields in haxe, we use the super-class's field type instead
declaredMembers = declaredMembers.map(m -> {
return if (m.flags & SymbolFlags.PropertyOrAccessor != 0) {
// @! we should find the first definition
var matchingClassSuperMember = classSuperMembers.find(sm -> sm.name == m.name);
var ret:Symbol = (matchingClassSuperMember != null ? matchingClassSuperMember : m);
ret;
} else m;
});
}
*/
var fields = generateTypeFields(
symbol,
access,
declaration,
[],
callSignatures,
tc.getIndexSignaturesOfType(declaredType),
declaredMembers,
typePath.name
);
// remove disallowed accessors, since this is a structure type in haxe, the only allowed accessor is `final`
for (field in fields) {
if (field.access != null) field.access = field.access.filter(a -> switch a {
case AFinal, ADynamic: true;
default: false;
});
};
// (we resolve field collisions here because the later pass doesn't alias to anon fields)
fields.resolveNameCollisions();
TDAlias(TAnonymous(fields));
}
return {
pack: typePath.pack,
name: typePath.name,
fields: [],
kind: kind,
params: typeParamDeclFromTypeDeclarationSymbol(symbol, access, declaration),
isExtern: false,
doc: getDoc(symbol),
meta: [],
pos: pos,
tsSymbol: symbol,
tsSymbolAccess: access,
};
}
function generateTypeFields(
symbol: Symbol,
access: SymbolAccess,
declaration: Null<Declaration>,
constructorSignatures: Array<Signature>,
callSignatures: Array<Signature>,
indexSignatures: Array<Signature>,
classMembers: Array<Symbol>,
haxeClassName: String
) {
var fields = new Array<Field>();
if (constructorSignatures.length > 0) {
fields.push(newFieldFromSignatures(constructorSignatures, symbol, access, declaration));
}
if (callSignatures.length > 0) {
// Log.log('\t<red>callSignatures <b>${callSignatures.length}</></>', callSignatures[0].declaration);
fields.push(functionFieldFromCallSignatures(selfCallFunctionName, callSignatures, symbol, access, declaration));
}
if (symbol.flags & SymbolFlags.Function != 0) {
// type symbol is a function, make a @:selfCall field
var tsType = getTsTypeOfField(symbol);
var signatures = tc.getSignaturesOfType(tc.getNonNullableType(tsType), Call);
var selfCallStatic = functionFieldFromCallSignatures('call', signatures, symbol, access, declaration);
selfCallStatic.enableAccess(AStatic);
fields.push(selfCallStatic);
} else if (
symbol.flags & SymbolFlags.Variable != 0 &&
access.match(ExportModule(_, _, [])) // specifically `export = variable` access
) {
// `export = variable`, transform to class with a static field called 'value'
// extern class Module {
// static var value(get, never): Int;
// static inline function get_value():Int return cast Module;
// }
// this is the variable equivalent of @:selfCall
var field = fieldFromSymbol('value', symbol, symbol, access, declaration);
switch field.kind {
case FVar(type, _), FProp(_, _, type, _):
field.kind = FProp('get', 'never', type);
field.access = [AStatic];
fields.push(field);
// add value getter implementation
fields.push((macro class {
static inline function get_value():$type return cast $i{haxeClassName};
}).fields[0]);
case FFun(f):
// it's possible for the variable to have a function type, in which case we make it a regular selfCall (same as above)
var tsType = getTsTypeOfField(symbol);
var signatures = tc.getSignaturesOfType(tc.getNonNullableType(tsType), Call);
var selfCallStatic = functionFieldFromCallSignatures('call', signatures, symbol, access, declaration);
selfCallStatic.enableAccess(AStatic);
fields.push(selfCallStatic);
}
}
if (indexSignatures.length > 0) {
// this is different from a _constructor_ declaration
Log.warn('Index signatures are not yet supported', symbol);
}
// class-fields
for (classMember in classMembers) {
// Log.log('\t<green>classMember</>', classMember);
fields.push(fieldFromSymbol(classMember.name, classMember, symbol, access, declaration));
}
return fields;
}
/**
Return true if the type will be represented as a structure in haxe
@! needs review
**/
@:pure function isTypeStructureInHaxe(type: TsType, moduleSymbol: Symbol, accessContext: SymbolAccess, ?enclosingDeclaration: Node): Bool {
if (type.flags & TypeFlags.Object != 0) {
// check the type can be converted
if (isHxAny(complexTypeFromTsType(type, moduleSymbol, accessContext, enclosingDeclaration))) {
return false;
}
var objectType: ObjectType = cast type;
var isAnonType = objectType.objectFlags & ObjectFlags.Anonymous != 0;
var isInterface = type.symbol != null && type.symbol.flags & SymbolFlags.Interface != 0;
var isValueModule = type.symbol != null && type.symbol.flags & SymbolFlags.ValueModule != 0;
var isConstructorType = tc.isConstructorType(objectType); // constructor types are converted to classes
var appearsToBeStructure = !isConstructorType && !isValueModule && (isAnonType || isInterface);
if (appearsToBeStructure) {
// @! todo
// assume any types in the haxe standard library are not structures. @! This can be improved in the future
// var hxTypePath = haxeTypePathMap.getTypePath(type.symbol, accessContext, false);
// if (hxTypePath.isExistingStdLibType) {
// return false;
// }
}
return appearsToBeStructure;
} else {
return false;
}
}
/**
A key that uniquely identifies a haxe module in a haxe project
Lower case string to represent module file case-insensitive collisions
`a.b.C` -> `a/b/c`
**/
function getHaxeModuleKey(pack: Array<String>, name: String) {
return pack.concat([name]).map(s -> s.toLowerCase()).join('/');
}
function saveHaxeModule(module: HaxeModule) {
var isBuiltIn = module.tsSymbol != null && module.tsSymbol.isBuiltIn();
var skipModule = false;
// skip global if globalTypes are disabled
if (!options.globalTypes) {
skipModule = skipModule || (!isBuiltIn && module.tsSymbolAccess.match(Global(_)));
}
// skip modular if modularTypes are disabled
if (!options.modularTypes) {
skipModule = skipModule || (!isBuiltIn && module.tsSymbolAccess.match(AmbientModule(_) | ExportModule(_)));
}
if (skipModule) return;
var path = getHaxeModuleKey(module.pack, module.name);
var existingModule = generatedModules.get(path);
if (existingModule != null) {
Log.warn('<red><b>saveHaxeModule():</> Module <b>"$path"</> has already been generated once and will be overwritten</>');
}
if (generatedModules.exists(path)) {
debug();
}
generatedModules.set(path, module);
}
function getDoc(symbol: Symbol) {
var sourceLocationInfo = [];
if (options.locationComments) {
var node = if (symbol.valueDeclaration != null) {
symbol.valueDeclaration;
} else {
symbol.getDeclarationsArray()[0];
}
if (node != null) {
var sourceFile = node.getSourceFile();
if (sourceFile != null) {
var start = node.getStart();
var lineAndCharacter = sourceFile.getLineAndCharacterOfPosition(start);
var line = lineAndCharacter.line;
var character = lineAndCharacter.character;
sourceLocationInfo.push('${FileTools.cwdRelativeFilePath(sourceFile.fileName)}:${line + 1}${character > 0 ? ':${character + 1}' : ''}');
}
}
}
return symbol.getDocumentationComment(tc)
.map(s -> s.text.trim())
.concat(sourceLocationInfo)
.join('\n');
}
// the type-stack is used to detect loops in recursive type conversions
var _currentTypeStack: Array<TsType> = [];
var _rasterizeMappedTypes = true;
/**
- `moduleSymbol` - is the module where this type is used. It's main use is to shorten type paths when referencing types in the same haxe module
- `accessContext` - is the symbol access path for the symbol that contains this type reference. This is required because if we're in a Global access context, type references should prefer global access (and modular context should prefer modular access). For example, in node.js there's a type `EventEmitter` that has both global (`NodeJS.EventEmitter` and modular access `require("event").EventEmitter`). If `EventEmitter` is referenced by another globally accessible type, then this method should return the global haxe type, and same logic for modular
- `preferInterfaceStructure` - set to true return the interface-structure version of a type in haxe. This is not handled recursively, so only the top-level reference will prefer-interface-structure
**/
function complexTypeFromTsType(type: TsType, moduleSymbol: Symbol, accessContext: SymbolAccess, ?enclosingDeclaration: Node, ?disallowAliasTarget: Symbol, preferInterfaceStructure: Bool = false): ComplexType {
// alias : this -> real type
if (type.isThisType()) {
var thisTarget = type.getThisTypeTarget();
if (thisTarget != null) {
type = thisTarget;
}
}
// print current type stack
// Log.log('typestack: [${_currentTypeStack.map(t -> t.getId()).join(',')}] + ${type.getId()}', type);
if (_currentTypeStack.length >= typeStackLimit) {
Log.error('Internal error: Reached type-depth limit, stopping further type conversions. This indicates unbound recursive type conversion');
debug();
return macro :Dynamic;
}
var stackHasType = _currentTypeStack.has(type);
// handle direct type aliases
// we deliberately break out of type-stack recursion checking here
if (type.aliasSymbol != null && disallowAliasTarget != type.aliasSymbol) {
_currentTypeStack.push(type);
var isAliasToMappedType = type.flags & TypeFlags.Object != 0 && (cast type: ObjectType).objectFlags & ObjectFlags.Mapped != 0;
var argsLength = type.aliasTypeArguments != null ? type.aliasTypeArguments.length : -1;
var hxType = switch {
name: type.aliasSymbol.name,
args: type.aliasTypeArguments,
isMappedType: isAliasToMappedType,
isBuiltIn: type.aliasSymbol.isBuiltIn(),
} {
// special case handling of built-in utility types
case { name: 'NonNullable', args: [t], isBuiltIn: true }:
if (t.isUnion()) {
// remove null from union
complexTypeFromUnionType(cast t, moduleSymbol, accessContext, enclosingDeclaration).unwrapNull();
} else {
complexTypeFromTsType(t, moduleSymbol, accessContext, enclosingDeclaration);
}
case { name: 'Partial' | 'Readonly', args: [t], isBuiltIn: true }:
if (tc.getPropertiesOfType(t).length > 0) {
complexTypeAnonFromTsType(type, moduleSymbol, accessContext, enclosingDeclaration);
} else {
// pass through type parameter
complexTypeFromTsType(t, moduleSymbol, accessContext, enclosingDeclaration);
}
case { name: 'Record' | 'Pick' | 'Omit' | 'Exclude' | 'Extract', args: [k, t], isBuiltIn: true }:
complexTypeAnonFromTsType(type, moduleSymbol, accessContext, enclosingDeclaration);
// rasterize other mapped types to objects
case { isMappedType: true, args: args } if (_rasterizeMappedTypes && !stackHasType):
// when resolving aliases, we're outside the type-stack recursion check so to help avoid recursion, we only allow 1 level of mapped-type rasterization
_rasterizeMappedTypes = false;
// special handling of mapped types
// we rasterize them to anons because we don't support them natively yet (though we could use macro support types for this)
var t = complexTypeAnonFromTsType(type, moduleSymbol, accessContext, enclosingDeclaration);
_rasterizeMappedTypes = true;
t;
default:
// haxe type alias
var haxeTypePath = getReferencedHaxeTypePath(type.aliasSymbol, moduleSymbol, accessContext, preferInterfaceStructure);
var params = if (type.aliasTypeArguments != null) {
type.aliasTypeArguments.map(t -> TPType(complexTypeFromTsType(t, moduleSymbol, accessContext, enclosingDeclaration)));
} else [];
TPath({
pack: haxeTypePath.pack,
name: haxeTypePath.name,
params: params
});
}
_currentTypeStack.pop();
return hxType;
}
if (stackHasType) {
Log.log('Breaking recursive type conversion', type);
// debug();
return macro :Dynamic;
}
_currentTypeStack.push(type);
// handle fundamental type flags
var complexType = try if (type.flags & (TypeFlags.Any) != 0) {
macro :Dynamic;
} else if (type.flags & (TypeFlags.Unknown | TypeFlags.Never) != 0) {
// we can't really represent `unknown` or `never` in haxe at the moment
macro :Any;
} else if (type.flags & (TypeFlags.String) != 0) {
macro :String;
} else if (type.flags & (TypeFlags.Number) != 0) {
macro :Float;
} else if (type.flags & (TypeFlags.Boolean) != 0) {
macro :Bool;