@runtime-type-inspector/transpiler 5.0.1 → 5.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs +1522 -296
- package/index.mjs +1419 -288
- package/package.json +2 -2
package/index.mjs
CHANGED
|
@@ -161,6 +161,8 @@ function toSourceTS(node) {
|
|
|
161
161
|
// parseType("string" ).kind === ts.SyntaxKind.StringKeyword
|
|
162
162
|
StringLiteral,
|
|
163
163
|
// parseType("'test'" ).literal.kind === ts.SyntaxKind.StringLiteral
|
|
164
|
+
SymbolKeyword,
|
|
165
|
+
// parseType("symbol" ).kind === ts.SyntaxKind.SymbolKeyword
|
|
164
166
|
ThisType,
|
|
165
167
|
// parseType("this" ).kind === ts.SyntaxKind.ThisType
|
|
166
168
|
TupleType,
|
|
@@ -212,7 +214,11 @@ function toSourceTS(node) {
|
|
|
212
214
|
NamedTupleMember,
|
|
213
215
|
// parseType('[a: 1]' ).elements[0].kind === ts.SyntaxKind.NamedTupleMember
|
|
214
216
|
MappedType,
|
|
215
|
-
// parseType('{[K in TaskType]: 123}' ).kind ===
|
|
217
|
+
// parseType('{[K in TaskType]: 123}' ).kind === ts.SyntaxKind.MappedType
|
|
218
|
+
MinusToken,
|
|
219
|
+
// parseType('{[K in TaskType]-?: 123}' ).questionToken.kind === ts.SyntaxKind.MinusToken
|
|
220
|
+
PlusToken,
|
|
221
|
+
// parseType('{[K in TaskType]+?: 123}' ).questionToken.kind === ts.SyntaxKind.PlusToken
|
|
216
222
|
TypeParameter,
|
|
217
223
|
// parseType('{[K in TaskType]: 123}' ).typeParameter.kind === ts.SyntaxKind.TypeParameter
|
|
218
224
|
QualifiedName,
|
|
@@ -329,12 +335,26 @@ function toSourceTS(node) {
|
|
|
329
335
|
// For example: {[K in TaskType]: InstanceType etc.
|
|
330
336
|
const iterable = toSourceTS(parameter.constraint); // TaskType
|
|
331
337
|
const element = toSourceTS(parameter.name); // K
|
|
332
|
-
|
|
338
|
+
const out = {
|
|
333
339
|
type: 'mapping',
|
|
334
340
|
iterable,
|
|
335
341
|
element,
|
|
336
342
|
result
|
|
337
343
|
};
|
|
344
|
+
if (node.nameType) {
|
|
345
|
+
// `as` key remapping, e.g. {[K in keyof T as K extends string ? K : never]: ...}
|
|
346
|
+
out.nameType = toSourceTS(node.nameType);
|
|
347
|
+
}
|
|
348
|
+
// Modifiers: `-?` strips optionality, `+?`/`?` force it;
|
|
349
|
+
// `-readonly` strips readonly, `+readonly`/`readonly` force it.
|
|
350
|
+
// Absent modifiers preserve the source behavior (see createTypeFromMapping).
|
|
351
|
+
if (node.questionToken) {
|
|
352
|
+
out.question = node.questionToken.kind === MinusToken ? '-' : node.questionToken.kind === PlusToken ? '+' : '?';
|
|
353
|
+
}
|
|
354
|
+
if (node.readonlyToken) {
|
|
355
|
+
out.readonly = node.readonlyToken.kind === MinusToken ? '-' : node.readonlyToken.kind === PlusToken ? '+' : 'readonly';
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
338
358
|
}
|
|
339
359
|
console.warn("MappedType: expected TypeParameter");
|
|
340
360
|
return 'transpiler-error';
|
|
@@ -552,6 +572,13 @@ function toSourceTS(node) {
|
|
|
552
572
|
optional: true
|
|
553
573
|
};
|
|
554
574
|
}
|
|
575
|
+
if (Array.isArray(member.modifiers) && member.modifiers.some(modifier => modifier.kind === ReadonlyKeyword)) {
|
|
576
|
+
// Tracked for IfEquals-style comparisons; ignored by validation.
|
|
577
|
+
if (type && typeof type === 'object') type.readonly = true;else type = {
|
|
578
|
+
type,
|
|
579
|
+
readonly: true
|
|
580
|
+
};
|
|
581
|
+
}
|
|
555
582
|
properties[name] = type;
|
|
556
583
|
} else {
|
|
557
584
|
console.warn('TypeLiteral: unhandled member', member);
|
|
@@ -611,6 +638,7 @@ function toSourceTS(node) {
|
|
|
611
638
|
case AnyKeyword:
|
|
612
639
|
case BooleanKeyword:
|
|
613
640
|
case StringKeyword:
|
|
641
|
+
case SymbolKeyword:
|
|
614
642
|
case NeverKeyword:
|
|
615
643
|
case NullKeyword:
|
|
616
644
|
case NumberKeyword:
|
|
@@ -691,6 +719,7 @@ function toSourceTS(node) {
|
|
|
691
719
|
*/
|
|
692
720
|
/**
|
|
693
721
|
* Splits a string by a delimiter, ignoring delimiters nested inside <>, {}, [], ().
|
|
722
|
+
* Quote-aware: delimiters inside '...', "..." or `...` never split.
|
|
694
723
|
* @param {string} str - The string to split.
|
|
695
724
|
* @param {string} delimiter - Single character delimiter.
|
|
696
725
|
* @returns {string[]} Top-level split parts.
|
|
@@ -701,10 +730,19 @@ function splitTopLevel(str, delimiter) {
|
|
|
701
730
|
let depthCurly = 0;
|
|
702
731
|
let depthSquare = 0;
|
|
703
732
|
let depthParen = 0;
|
|
733
|
+
let inSingle = false;
|
|
734
|
+
let inDouble = false;
|
|
735
|
+
let inBacktick = false;
|
|
704
736
|
let current = '';
|
|
705
|
-
for (
|
|
706
|
-
|
|
707
|
-
|
|
737
|
+
for (let i = 0; i < str.length; i++) {
|
|
738
|
+
const c = str[i];
|
|
739
|
+
const prev = i > 0 ? str[i - 1] : '';
|
|
740
|
+
if (c === "'" && !inDouble && !inBacktick && prev !== '\\') inSingle = !inSingle;else if (c === '"' && !inSingle && !inBacktick && prev !== '\\') inDouble = !inDouble;else if (c === '`' && !inSingle && !inDouble && prev !== '\\') inBacktick = !inBacktick;
|
|
741
|
+
const inQuote = inSingle || inDouble || inBacktick;
|
|
742
|
+
if (!inQuote) {
|
|
743
|
+
if (c === '<') depthAngle++;else if (c === '>') depthAngle--;else if (c === '{') depthCurly++;else if (c === '}') depthCurly--;else if (c === '[') depthSquare++;else if (c === ']') depthSquare--;else if (c === '(') depthParen++;else if (c === ')') depthParen--;
|
|
744
|
+
}
|
|
745
|
+
if (c === delimiter && !inQuote && depthAngle === 0 && depthCurly === 0 && depthSquare === 0 && depthParen === 0) {
|
|
708
746
|
parts.push(current);
|
|
709
747
|
current = '';
|
|
710
748
|
} else {
|
|
@@ -714,6 +752,123 @@ function splitTopLevel(str, delimiter) {
|
|
|
714
752
|
parts.push(current);
|
|
715
753
|
return parts;
|
|
716
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Depth state at a given index, quote-aware.
|
|
757
|
+
* @param {string} str - The string to scan.
|
|
758
|
+
* @param {number} upto - Exclusive end index.
|
|
759
|
+
* @returns {{angle: number, curly: number, square: number, paren: number, quote: boolean}} Depths.
|
|
760
|
+
*/
|
|
761
|
+
function depthsAt(str, upto) {
|
|
762
|
+
let angle = 0;
|
|
763
|
+
let curly = 0;
|
|
764
|
+
let square = 0;
|
|
765
|
+
let paren = 0;
|
|
766
|
+
let inSingle = false;
|
|
767
|
+
let inDouble = false;
|
|
768
|
+
let inBacktick = false;
|
|
769
|
+
for (let i = 0; i < upto; i++) {
|
|
770
|
+
const c = str[i];
|
|
771
|
+
const prev = i > 0 ? str[i - 1] : '';
|
|
772
|
+
if (c === "'" && !inDouble && !inBacktick && prev !== '\\') inSingle = !inSingle;else if (c === '"' && !inSingle && !inBacktick && prev !== '\\') inDouble = !inDouble;else if (c === '`' && !inSingle && !inDouble && prev !== '\\') inBacktick = !inBacktick;
|
|
773
|
+
const inQuote = inSingle || inDouble || inBacktick;
|
|
774
|
+
if (inQuote) continue;
|
|
775
|
+
if (c === '<') angle++;else if (c === '>') angle--;else if (c === '{') curly++;else if (c === '}') curly--;else if (c === '[') square++;else if (c === ']') square--;else if (c === '(') paren++;else if (c === ')') paren--;
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
angle,
|
|
779
|
+
curly,
|
|
780
|
+
square,
|
|
781
|
+
paren,
|
|
782
|
+
quote: inSingle || inDouble || inBacktick
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Finds a keyword (e.g. `in`, `as`, `extends`) at top level, outside brackets/quotes.
|
|
787
|
+
* @param {string} str - The string to search.
|
|
788
|
+
* @param {string} keyword - Keyword without surrounding spaces.
|
|
789
|
+
* @returns {number} Start index or -1.
|
|
790
|
+
*/
|
|
791
|
+
function findTopLevelKeyword(str, keyword) {
|
|
792
|
+
const isWord = c => /[A-Za-z0-9_$]/.test(c);
|
|
793
|
+
for (let i = 0; i <= str.length - keyword.length; i++) {
|
|
794
|
+
if (str.slice(i, i + keyword.length) !== keyword) continue;
|
|
795
|
+
const before = i > 0 ? str[i - 1] : ' ';
|
|
796
|
+
const after = i + keyword.length < str.length ? str[i + keyword.length] : ' ';
|
|
797
|
+
if (isWord(before) || isWord(after)) continue;
|
|
798
|
+
const d = depthsAt(str, i);
|
|
799
|
+
if (d.angle === 0 && d.curly === 0 && d.square === 0 && d.paren === 0 && !d.quote) {
|
|
800
|
+
// Ensure remainder after keyword is also top-level (keyword itself not quoted).
|
|
801
|
+
const d2 = depthsAt(str, i + keyword.length);
|
|
802
|
+
if (!d2.quote) return i;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return -1;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Finds a single-char delimiter at top level, outside brackets/quotes.
|
|
809
|
+
* @param {string} str - The string to search.
|
|
810
|
+
* @param {string} delimiter - Single character.
|
|
811
|
+
* @param {number} from - Start index.
|
|
812
|
+
* @returns {number} Index or -1.
|
|
813
|
+
*/
|
|
814
|
+
function findTopLevelChar(str, delimiter, from = 0) {
|
|
815
|
+
let angle = 0;
|
|
816
|
+
let curly = 0;
|
|
817
|
+
let square = 0;
|
|
818
|
+
let paren = 0;
|
|
819
|
+
let inSingle = false;
|
|
820
|
+
let inDouble = false;
|
|
821
|
+
let inBacktick = false;
|
|
822
|
+
for (let i = 0; i < str.length; i++) {
|
|
823
|
+
const c = str[i];
|
|
824
|
+
// Check at depths *before* the current char, so `[` itself is findable.
|
|
825
|
+
const inQuoteBefore = inSingle || inDouble || inBacktick;
|
|
826
|
+
if (i >= from && c === delimiter && !inQuoteBefore && angle === 0 && curly === 0 && square === 0 && paren === 0) {
|
|
827
|
+
return i;
|
|
828
|
+
}
|
|
829
|
+
const prev = i > 0 ? str[i - 1] : '';
|
|
830
|
+
if (c === "'" && !inDouble && !inBacktick && prev !== '\\') inSingle = !inSingle;else if (c === '"' && !inSingle && !inBacktick && prev !== '\\') inDouble = !inDouble;else if (c === '`' && !inSingle && !inDouble && prev !== '\\') inBacktick = !inBacktick;
|
|
831
|
+
const inQuote = inSingle || inDouble || inBacktick;
|
|
832
|
+
if (!inQuote) {
|
|
833
|
+
if (c === '<') angle++;else if (c === '>') angle--;else if (c === '{') curly++;else if (c === '}') curly--;else if (c === '[') square++;else if (c === ']') square--;else if (c === '(') paren++;else if (c === ')') paren--;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return -1;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Strips balanced outer parens, e.g. `(( T ))` -> `T`.
|
|
840
|
+
* @param {string} type - Trimmed type string.
|
|
841
|
+
* @returns {string} Unwrapped type.
|
|
842
|
+
*/
|
|
843
|
+
function stripOuterParens(type) {
|
|
844
|
+
let changed = true;
|
|
845
|
+
while (changed) {
|
|
846
|
+
changed = false;
|
|
847
|
+
if (type.length >= 2 && type[0] === '(' && type[type.length - 1] === ')') {
|
|
848
|
+
let depth = 0;
|
|
849
|
+
let balanced = true;
|
|
850
|
+
let inSingle = false;
|
|
851
|
+
let inDouble = false;
|
|
852
|
+
for (let i = 0; i < type.length; i++) {
|
|
853
|
+
const c = type[i];
|
|
854
|
+
if (c === "'" && !inDouble && type[i - 1] !== '\\') inSingle = !inSingle;else if (c === '"' && !inSingle && type[i - 1] !== '\\') inDouble = !inDouble;else if (!inSingle && !inDouble) {
|
|
855
|
+
if (c === '(') depth++;else if (c === ')') {
|
|
856
|
+
depth--;
|
|
857
|
+
if (depth === 0 && i !== type.length - 1) {
|
|
858
|
+
balanced = false;
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (balanced && depth === 0) {
|
|
865
|
+
type = type.slice(1, -1).trim();
|
|
866
|
+
changed = true;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return type;
|
|
871
|
+
}
|
|
717
872
|
/**
|
|
718
873
|
* Parses `Name<A, B>` into name + raw arg strings, respecting nested brackets.
|
|
719
874
|
* Returns undefined when input isn't a generic reference.
|
|
@@ -754,6 +909,190 @@ function parseGenericReference(type) {
|
|
|
754
909
|
args
|
|
755
910
|
};
|
|
756
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Parses `{[K in Iterable as NameType]?: Result}` mapped syntax.
|
|
914
|
+
* Returns undefined when input isn't a mapped type.
|
|
915
|
+
* @param {string} type - Trimmed type string starting with `{`.
|
|
916
|
+
* @returns {object|undefined} Mapping struct with raw strings, or undefined.
|
|
917
|
+
*/
|
|
918
|
+
function parseMappedRaw(type) {
|
|
919
|
+
if (type[0] !== '{' || type[type.length - 1] !== '}') {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
let inner = type.slice(1, -1).trim();
|
|
923
|
+
let readonly;
|
|
924
|
+
if (inner.startsWith('-readonly') && (inner[9] === ' ' || inner[9] === '[')) {
|
|
925
|
+
readonly = '-';
|
|
926
|
+
inner = inner.slice(9).trim();
|
|
927
|
+
} else if (inner.startsWith('+readonly') && (inner[9] === ' ' || inner[9] === '[')) {
|
|
928
|
+
readonly = '+';
|
|
929
|
+
inner = inner.slice(9).trim();
|
|
930
|
+
} else if (inner.startsWith('readonly') && (inner[8] === ' ' || inner[8] === '[')) {
|
|
931
|
+
readonly = 'readonly';
|
|
932
|
+
inner = inner.slice(8).trim();
|
|
933
|
+
}
|
|
934
|
+
if (inner[0] !== '[') {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
// Find matching `]` for the opening `[`, quote-aware.
|
|
938
|
+
let square = 0;
|
|
939
|
+
let inSingle = false;
|
|
940
|
+
let inDouble = false;
|
|
941
|
+
let inBacktick = false;
|
|
942
|
+
let close = -1;
|
|
943
|
+
for (let i = 0; i < inner.length; i++) {
|
|
944
|
+
const c = inner[i];
|
|
945
|
+
const prev = i > 0 ? inner[i - 1] : '';
|
|
946
|
+
if (c === "'" && !inDouble && !inBacktick && prev !== '\\') inSingle = !inSingle;else if (c === '"' && !inSingle && !inBacktick && prev !== '\\') inDouble = !inDouble;else if (c === '`' && !inSingle && !inDouble && prev !== '\\') inBacktick = !inBacktick;else if (!inSingle && !inDouble && !inBacktick) {
|
|
947
|
+
if (c === '[') square++;else if (c === ']') {
|
|
948
|
+
square--;
|
|
949
|
+
if (square === 0) {
|
|
950
|
+
close = i;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (close === -1) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const bracket = inner.slice(1, close);
|
|
960
|
+
let rest = inner.slice(close + 1).trim();
|
|
961
|
+
let question;
|
|
962
|
+
if (rest.startsWith('-?')) {
|
|
963
|
+
question = '-';
|
|
964
|
+
rest = rest.slice(2).trim();
|
|
965
|
+
} else if (rest.startsWith('+?')) {
|
|
966
|
+
question = '+';
|
|
967
|
+
rest = rest.slice(2).trim();
|
|
968
|
+
} else if (rest.startsWith('?')) {
|
|
969
|
+
question = '?';
|
|
970
|
+
rest = rest.slice(1).trim();
|
|
971
|
+
}
|
|
972
|
+
if (!rest.startsWith(':')) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const resultRaw = rest.slice(1).trim();
|
|
976
|
+
if (!resultRaw) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const inIdx = findTopLevelKeyword(bracket, 'in');
|
|
980
|
+
if (inIdx === -1) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const element = bracket.slice(0, inIdx).trim();
|
|
984
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(element)) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const afterIn = bracket.slice(inIdx + 2).trim();
|
|
988
|
+
if (!afterIn) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const asIdx = findTopLevelKeyword(afterIn, 'as');
|
|
992
|
+
let iterableRaw = afterIn;
|
|
993
|
+
let nameRaw;
|
|
994
|
+
if (asIdx !== -1) {
|
|
995
|
+
iterableRaw = afterIn.slice(0, asIdx).trim();
|
|
996
|
+
nameRaw = afterIn.slice(asIdx + 2).trim();
|
|
997
|
+
if (!iterableRaw || !nameRaw) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return {
|
|
1002
|
+
element,
|
|
1003
|
+
iterableRaw,
|
|
1004
|
+
nameRaw,
|
|
1005
|
+
resultRaw,
|
|
1006
|
+
question,
|
|
1007
|
+
readonly
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Splits `Check extends Extends ? True : False` at top level.
|
|
1012
|
+
* Returns undefined when input isn't a conditional type.
|
|
1013
|
+
* @param {string} type - Trimmed type string.
|
|
1014
|
+
* @returns {{check: string, extends_: string, true_: string, false_: string}|undefined} Raw parts.
|
|
1015
|
+
*/
|
|
1016
|
+
function parseConditionRaw(type) {
|
|
1017
|
+
const qIdx = findTopLevelChar(type, '?');
|
|
1018
|
+
if (qIdx === -1) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const beforeQ = type.slice(0, qIdx);
|
|
1022
|
+
const afterQ = type.slice(qIdx + 1);
|
|
1023
|
+
const extIdx = findTopLevelKeyword(beforeQ, 'extends');
|
|
1024
|
+
if (extIdx === -1) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const colonIdx = findTopLevelChar(afterQ, ':');
|
|
1028
|
+
if (colonIdx === -1) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const check = beforeQ.slice(0, extIdx).trim();
|
|
1032
|
+
const extends_ = beforeQ.slice(extIdx + 7).trim();
|
|
1033
|
+
const true_ = afterQ.slice(0, colonIdx).trim();
|
|
1034
|
+
const false_ = afterQ.slice(colonIdx + 1).trim();
|
|
1035
|
+
if (!check || !extends_ || !true_ || !false_) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
// Guard against `?.` optional chaining and `??` nullish coalescing.
|
|
1039
|
+
if (type[qIdx + 1] === '.' || type[qIdx + 1] === '?') {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
check,
|
|
1044
|
+
extends_,
|
|
1045
|
+
true_,
|
|
1046
|
+
false_
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Splits `Object[Index]` at top level. Excludes tuples (`[...]`) and
|
|
1051
|
+
* empty-index arrays (`T[]`, handled earlier).
|
|
1052
|
+
* @param {string} type - Trimmed type string.
|
|
1053
|
+
* @returns {{objectRaw: string, indexRaw: string}|undefined} Raw parts.
|
|
1054
|
+
*/
|
|
1055
|
+
function parseIndexedAccessRaw(type) {
|
|
1056
|
+
if (!type.endsWith(']') || type.endsWith('[]')) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (type[0] === '[') {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const openIdx = findTopLevelChar(type, '[');
|
|
1063
|
+
if (openIdx === -1) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
// Ensure the `[` at openIdx closes at the very end.
|
|
1067
|
+
let square = 0;
|
|
1068
|
+
let inSingle = false;
|
|
1069
|
+
let inDouble = false;
|
|
1070
|
+
let inBacktick = false;
|
|
1071
|
+
for (let i = openIdx; i < type.length; i++) {
|
|
1072
|
+
const c = type[i];
|
|
1073
|
+
const prev = i > 0 ? type[i - 1] : '';
|
|
1074
|
+
if (c === "'" && !inDouble && !inBacktick && prev !== '\\') inSingle = !inSingle;else if (c === '"' && !inSingle && !inBacktick && prev !== '\\') inDouble = !inDouble;else if (c === '`' && !inSingle && !inDouble && prev !== '\\') inBacktick = !inBacktick;else if (!inSingle && !inDouble && !inBacktick) {
|
|
1075
|
+
if (c === '[') square++;else if (c === ']') {
|
|
1076
|
+
square--;
|
|
1077
|
+
if (square === 0 && i !== type.length - 1) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (square !== 0) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const objectRaw = type.slice(0, openIdx).trim();
|
|
1087
|
+
const indexRaw = type.slice(openIdx + 1, -1).trim();
|
|
1088
|
+
if (!objectRaw || !indexRaw) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
objectRaw,
|
|
1093
|
+
indexRaw
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
757
1096
|
/**
|
|
758
1097
|
* 'DepFree' refers to the fact that this function has no dependencies,
|
|
759
1098
|
* while `expandType` depends on TypeScript itself for maximum compatibility.
|
|
@@ -769,6 +1108,33 @@ function parseGenericReference(type) {
|
|
|
769
1108
|
*/
|
|
770
1109
|
function expandTypeDepFree(type) {
|
|
771
1110
|
type = type.trim();
|
|
1111
|
+
// '(123)' -> '123': strip balanced outer parens first so `(cond)?`
|
|
1112
|
+
// nullable and `(A|B)` unions see through them.
|
|
1113
|
+
const stripped = stripOuterParens(type);
|
|
1114
|
+
if (stripped !== type) {
|
|
1115
|
+
return expandTypeDepFree(stripped);
|
|
1116
|
+
}
|
|
1117
|
+
// `readonly T` erased at runtime, same shape as the inner type.
|
|
1118
|
+
if (type.startsWith('readonly ') && type.length > 9) {
|
|
1119
|
+
return expandTypeDepFree(type.slice(9).trim());
|
|
1120
|
+
}
|
|
1121
|
+
if (type === 'unique symbol') {
|
|
1122
|
+
return 'any';
|
|
1123
|
+
}
|
|
1124
|
+
// Conditionals before nullable: `A extends B ? C : D?` must keep the
|
|
1125
|
+
// nullable on the false branch, not lift it over the whole condition.
|
|
1126
|
+
// (`(cond)?` has no top-level `?` due to parens, so it falls through
|
|
1127
|
+
// to nullable below.)
|
|
1128
|
+
const earlyCond = parseConditionRaw(type);
|
|
1129
|
+
if (earlyCond) {
|
|
1130
|
+
return {
|
|
1131
|
+
type: 'condition',
|
|
1132
|
+
checkType: expandTypeDepFree(earlyCond.check),
|
|
1133
|
+
extendsType: expandTypeDepFree(earlyCond.extends_),
|
|
1134
|
+
trueType: expandTypeDepFree(earlyCond.true_),
|
|
1135
|
+
falseType: expandTypeDepFree(earlyCond.false_)
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
772
1138
|
// JSDocNullableType (`T?` / `?T`): union with null, matching expandType().
|
|
773
1139
|
if (type.endsWith('?') && type.length > 1) {
|
|
774
1140
|
return {
|
|
@@ -782,20 +1148,9 @@ function expandTypeDepFree(type) {
|
|
|
782
1148
|
members: [expandTypeDepFree(type.slice(1).trim()), 'null']
|
|
783
1149
|
};
|
|
784
1150
|
}
|
|
785
|
-
// `readonly T` erased at runtime, same shape as the inner type.
|
|
786
|
-
if (type.startsWith('readonly ') && type.length > 9) {
|
|
787
|
-
return expandTypeDepFree(type.slice(9).trim());
|
|
788
|
-
}
|
|
789
|
-
if (type === 'unique symbol') {
|
|
790
|
-
return 'any';
|
|
791
|
-
}
|
|
792
|
-
// '(123)' -> '123'
|
|
793
|
-
while (!type.includes('|') && type[0] === '(' && type[type.length - 1] === ')') {
|
|
794
|
-
type = type.slice(1, -1).trim();
|
|
795
|
-
}
|
|
796
1151
|
// (1) Rest parameters like ...string
|
|
797
1152
|
if (type[0] === '.' && type[1] === '.' && type[2] === '.') {
|
|
798
|
-
const elementType = type.slice(3);
|
|
1153
|
+
const elementType = expandTypeDepFree(type.slice(3).trim());
|
|
799
1154
|
return {
|
|
800
1155
|
type: 'array',
|
|
801
1156
|
elementType
|
|
@@ -823,17 +1178,18 @@ function expandTypeDepFree(type) {
|
|
|
823
1178
|
// (3) Object<...> or Record<...>
|
|
824
1179
|
if ((type.startsWith("Object<") || type.startsWith("Record<")) && type.endsWith('>')) {
|
|
825
1180
|
const recordSlice = type.slice(7, -1);
|
|
826
|
-
const
|
|
827
|
-
if (
|
|
1181
|
+
const commaIdx = findTopLevelChar(recordSlice, ',');
|
|
1182
|
+
if (commaIdx === -1) {
|
|
828
1183
|
console.warn("expandTypeDepFree> invalid Object/Record");
|
|
1184
|
+
} else {
|
|
1185
|
+
const key = recordSlice.slice(0, commaIdx).trim();
|
|
1186
|
+
const val = recordSlice.slice(commaIdx + 1).trim();
|
|
1187
|
+
return {
|
|
1188
|
+
type: "record",
|
|
1189
|
+
key: expandTypeDepFree(key),
|
|
1190
|
+
val: expandTypeDepFree(val)
|
|
1191
|
+
};
|
|
829
1192
|
}
|
|
830
|
-
const key = recordSlice.slice(0, firstComma).trim();
|
|
831
|
-
const val = recordSlice.slice(firstComma + 1).trim();
|
|
832
|
-
return {
|
|
833
|
-
type: "record",
|
|
834
|
-
key: expandTypeDepFree(key),
|
|
835
|
-
val: expandTypeDepFree(val)
|
|
836
|
-
};
|
|
837
1193
|
}
|
|
838
1194
|
// (3b) Map<...> / Set<...' for dep-free parity with expandType()
|
|
839
1195
|
if (type.startsWith("Map<") && type.endsWith('>')) {
|
|
@@ -868,33 +1224,96 @@ function expandTypeDepFree(type) {
|
|
|
868
1224
|
args: args.map(expandTypeDepFree)
|
|
869
1225
|
};
|
|
870
1226
|
}
|
|
871
|
-
// (4) {
|
|
1227
|
+
// (4) Mapped types `{[K in X as Y]?: Z}` before the object-literal fallback.
|
|
872
1228
|
if (type[0] === '{' && type[type.length - 1] === '}') {
|
|
873
|
-
const
|
|
1229
|
+
const mapped = parseMappedRaw(type);
|
|
1230
|
+
if (mapped) {
|
|
1231
|
+
const out = {
|
|
1232
|
+
type: 'mapping',
|
|
1233
|
+
iterable: expandTypeDepFree(mapped.iterableRaw),
|
|
1234
|
+
element: mapped.element,
|
|
1235
|
+
result: expandTypeDepFree(mapped.resultRaw)
|
|
1236
|
+
};
|
|
1237
|
+
if (mapped.nameRaw !== undefined) {
|
|
1238
|
+
out.nameType = expandTypeDepFree(mapped.nameRaw);
|
|
1239
|
+
}
|
|
1240
|
+
if (mapped.question !== undefined) {
|
|
1241
|
+
out.question = mapped.question;
|
|
1242
|
+
}
|
|
1243
|
+
if (mapped.readonly !== undefined) {
|
|
1244
|
+
out.readonly = mapped.readonly;
|
|
1245
|
+
}
|
|
1246
|
+
return out;
|
|
1247
|
+
}
|
|
1248
|
+
const propertiesArray = splitTopLevel(type.slice(1, -1), ','); // ['entity: Entity', ' app: AppBase']
|
|
874
1249
|
const properties = {};
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
-
if (
|
|
1250
|
+
for (const entry of propertiesArray) {
|
|
1251
|
+
const colonIdx = findTopLevelChar(entry, ':');
|
|
1252
|
+
if (colonIdx === -1) {
|
|
1253
|
+
// Empty `{}` yields one empty entry: the empty object type.
|
|
1254
|
+
if (entry.trim() === '') {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
878
1257
|
console.warn('expandTypeDepFree> unexpected type format, fix');
|
|
879
|
-
|
|
1258
|
+
continue;
|
|
880
1259
|
}
|
|
881
|
-
|
|
882
|
-
|
|
1260
|
+
const propName = entry.slice(0, colonIdx).trim();
|
|
1261
|
+
const propTypeRaw = entry.slice(colonIdx + 1).trim();
|
|
1262
|
+
if (!propName || !propTypeRaw) {
|
|
1263
|
+
console.warn('expandTypeDepFree> unexpected type format, fix');
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
properties[propName] = expandTypeDepFree(propTypeRaw);
|
|
1267
|
+
}
|
|
1268
|
+
// Bare `{}` carries no `properties` key, matching the TS/Babel parsers
|
|
1269
|
+
// (and distinguishing it from the `object` keyword, which does).
|
|
1270
|
+
if (!Object.keys(properties).length) {
|
|
1271
|
+
return {
|
|
1272
|
+
type: 'object'
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
883
1275
|
return {
|
|
884
1276
|
type: 'object',
|
|
885
1277
|
properties
|
|
886
1278
|
};
|
|
887
1279
|
}
|
|
888
|
-
// (5)
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1280
|
+
// (5) Unions and intersections via top-level splits (`&` binds tighter, so `|` first).
|
|
1281
|
+
// Note: conditionals already handled up front (before nullable) so `D?`
|
|
1282
|
+
// false branches keep their nullability; this slot is intentionally union-only.
|
|
1283
|
+
const unionParts = splitTopLevel(type, "|");
|
|
1284
|
+
if (unionParts.length >= 2) {
|
|
892
1285
|
return {
|
|
893
1286
|
type: 'union',
|
|
894
|
-
members:
|
|
1287
|
+
members: unionParts.map(_ => expandTypeDepFree(_.trim()))
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
const interParts = splitTopLevel(type, "&");
|
|
1291
|
+
if (interParts.length >= 2) {
|
|
1292
|
+
return {
|
|
1293
|
+
type: 'intersection',
|
|
1294
|
+
members: interParts.map(_ => expandTypeDepFree(_.trim()))
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
// (7) `keyof T` before typeof so `keyof typeof X` nests correctly.
|
|
1298
|
+
if (type.startsWith('keyof ') || type.startsWith('keyof(')) {
|
|
1299
|
+
const after = type.startsWith('keyof(') ? type.slice(5).trim() : type.slice(6).trim();
|
|
1300
|
+
if (after) {
|
|
1301
|
+
return {
|
|
1302
|
+
type: 'keyof',
|
|
1303
|
+
argument: expandTypeDepFree(after)
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
// (8) Indexed access `T[K]` (arrays `T[]` handled below, tuples above start with `[`).
|
|
1308
|
+
const indexed = parseIndexedAccessRaw(type);
|
|
1309
|
+
if (indexed) {
|
|
1310
|
+
return {
|
|
1311
|
+
type: 'indexedAccess',
|
|
1312
|
+
index: expandTypeDepFree(indexed.indexRaw),
|
|
1313
|
+
object: expandTypeDepFree(indexed.objectRaw)
|
|
895
1314
|
};
|
|
896
1315
|
}
|
|
897
|
-
// (
|
|
1316
|
+
// (9) expand [] Arrays
|
|
898
1317
|
// Test arrays: new pc.Mat3().set([1, 2, 3, "asd"])
|
|
899
1318
|
if (type.endsWith("[]")) {
|
|
900
1319
|
const typeSlice = type.slice(0, -2);
|
|
@@ -903,17 +1322,24 @@ function expandTypeDepFree(type) {
|
|
|
903
1322
|
elementType: expandTypeDepFree(typeSlice)
|
|
904
1323
|
};
|
|
905
1324
|
}
|
|
906
|
-
// (
|
|
1325
|
+
// (10) expand tuples
|
|
907
1326
|
if (type[0] === '[' && type[type.length - 1] === ']') {
|
|
908
|
-
const
|
|
1327
|
+
const inner = type.slice(1, -1);
|
|
1328
|
+
if (inner.trim() === '') {
|
|
1329
|
+
return {
|
|
1330
|
+
type: 'tuple',
|
|
1331
|
+
elements: []
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
const elements = splitTopLevel(inner, ','); // ['null', ' Texture', ...]
|
|
909
1335
|
return {
|
|
910
1336
|
type: 'tuple',
|
|
911
|
-
elements: elements.map(expandTypeDepFree)
|
|
1337
|
+
elements: elements.map(_ => expandTypeDepFree(_.trim()))
|
|
912
1338
|
};
|
|
913
1339
|
}
|
|
914
|
-
// (
|
|
1340
|
+
// (11) expand typeof expressions
|
|
915
1341
|
if (type.startsWith('typeof ')) {
|
|
916
|
-
const argument = expandTypeDepFree(type.substring(7));
|
|
1342
|
+
const argument = expandTypeDepFree(type.substring(7).trim());
|
|
917
1343
|
return {
|
|
918
1344
|
type: 'typeof',
|
|
919
1345
|
argument
|
|
@@ -939,81 +1365,19 @@ function expandTypeDepFree(type) {
|
|
|
939
1365
|
return type;
|
|
940
1366
|
}
|
|
941
1367
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
* @param {import('@babel/types').Node} node - The default value AST node.
|
|
950
|
-
* @returns {string | object | undefined} Inferred type or `undefined` to skip.
|
|
951
|
-
*/
|
|
952
|
-
function inferTypeFromDefault$1(node) {
|
|
953
|
-
if (!node) {
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
switch (node.type) {
|
|
957
|
-
case 'NumericLiteral':
|
|
958
|
-
return 'number';
|
|
959
|
-
case 'StringLiteral':
|
|
960
|
-
return 'string';
|
|
961
|
-
case 'BooleanLiteral':
|
|
962
|
-
return 'boolean';
|
|
963
|
-
case 'BigIntLiteral':
|
|
964
|
-
return {
|
|
965
|
-
type: 'bigint'
|
|
966
|
-
};
|
|
967
|
-
case 'RegExpLiteral':
|
|
968
|
-
return 'RegExp';
|
|
969
|
-
case 'TemplateLiteral':
|
|
970
|
-
return 'string';
|
|
971
|
-
case 'ArrayExpression':
|
|
972
|
-
return {
|
|
973
|
-
type: 'array',
|
|
974
|
-
elementType: 'any'
|
|
975
|
-
};
|
|
976
|
-
case 'ObjectExpression':
|
|
977
|
-
return {
|
|
978
|
-
type: 'object',
|
|
979
|
-
properties: {}
|
|
980
|
-
};
|
|
981
|
-
case 'ArrowFunctionExpression':
|
|
982
|
-
case 'FunctionExpression':
|
|
983
|
-
return 'Function';
|
|
984
|
-
case 'NewExpression':
|
|
985
|
-
{
|
|
986
|
-
const {
|
|
987
|
-
callee
|
|
988
|
-
} = node;
|
|
989
|
-
if (callee.type === 'Identifier') {
|
|
990
|
-
return callee.name;
|
|
991
|
-
}
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
case 'UnaryExpression':
|
|
995
|
-
{
|
|
996
|
-
const {
|
|
997
|
-
operator,
|
|
998
|
-
argument
|
|
999
|
-
} = node;
|
|
1000
|
-
if (operator === '!') {
|
|
1001
|
-
return 'boolean';
|
|
1002
|
-
}
|
|
1003
|
-
if (operator === 'void' || operator === 'typeof') {
|
|
1004
|
-
return operator === 'void' ? 'undefined' : 'string';
|
|
1005
|
-
}
|
|
1006
|
-
if ((operator === '-' || operator === '+') && argument.type === 'NumericLiteral') {
|
|
1007
|
-
return 'number';
|
|
1008
|
-
}
|
|
1009
|
-
if ((operator === '-' || operator === '+') && argument.type === 'BigIntLiteral') {
|
|
1010
|
-
return {
|
|
1011
|
-
type: 'bigint'
|
|
1012
|
-
};
|
|
1368
|
+
function _extends() {
|
|
1369
|
+
_extends = Object.assign ? Object.assign.bind() : function (target) {
|
|
1370
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
1371
|
+
var source = arguments[i];
|
|
1372
|
+
for (var key in source) {
|
|
1373
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1374
|
+
target[key] = source[key];
|
|
1013
1375
|
}
|
|
1014
|
-
break;
|
|
1015
1376
|
}
|
|
1016
|
-
|
|
1377
|
+
}
|
|
1378
|
+
return target;
|
|
1379
|
+
};
|
|
1380
|
+
return _extends.apply(this, arguments);
|
|
1017
1381
|
}
|
|
1018
1382
|
|
|
1019
1383
|
/**
|
|
@@ -1067,78 +1431,793 @@ function extractCurlyContent(line) {
|
|
|
1067
1431
|
}
|
|
1068
1432
|
if (count === -1) {
|
|
1069
1433
|
break;
|
|
1070
|
-
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
const content = line.substring(firstCurly + 1, k);
|
|
1437
|
+
return {
|
|
1438
|
+
content,
|
|
1439
|
+
nextIndex: k + 1
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Parses JSDoc comments to extract and expand typedefs and their associated properties.
|
|
1444
|
+
*
|
|
1445
|
+
* It iterates through the lines of a `CommentBlock` from the Babel AST, looking for `@typedef` and `@property`
|
|
1446
|
+
* annotations. When it finds a typedef, it stores it in the `typedefs` record. When it finds a property,
|
|
1447
|
+
* it adds it to the last found typedef if it is an object type. `@template` names preceding a
|
|
1448
|
+
* typedef are recorded in `typedefTemplates` so generic references can instantiate.
|
|
1449
|
+
* @param {Record<string, object>} typedefs - An object to store typedefs, mapping type names to their expanded definitions.
|
|
1450
|
+
* @param {Record<string, string[]>} typedefTemplates - An object to store template parameter names per generic typedef.
|
|
1451
|
+
* @param {Console["warn"]} warn - A warn function used for emitting warnings about non-extensible types.
|
|
1452
|
+
* @param {import("@babel/types").Comment} comment - A comment extracted from Babel's AST, expected to be a CommentBlock containing type definitions.
|
|
1453
|
+
* @param {Function} expandType - A function that takes a type expression as a string and returns a structured representation of the type.
|
|
1454
|
+
*/
|
|
1455
|
+
function parseJSDocTypedef(typedefs, typedefTemplates, warn, comment, expandType) {
|
|
1456
|
+
const {
|
|
1457
|
+
type,
|
|
1458
|
+
value
|
|
1459
|
+
} = comment;
|
|
1460
|
+
if (type !== 'CommentBlock') {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const lines = value.split('\n');
|
|
1464
|
+
let lastTypedef;
|
|
1465
|
+
let pendingTemplates = [];
|
|
1466
|
+
/**
|
|
1467
|
+
* @param {string} line - The trimmed JSDoc line.
|
|
1468
|
+
* @returns {boolean} True when the line held a template tag.
|
|
1469
|
+
*/
|
|
1470
|
+
function harvestTemplate(line) {
|
|
1471
|
+
let match = line.match(/@template \{(.*?)\} ([a-zA-Z0-9_$]+)/);
|
|
1472
|
+
if (match) {
|
|
1473
|
+
pendingTemplates.push(match[2]);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
match = line.match(/@template (?:\{.*?\} )?\[([a-zA-Z0-9_$]+)=/);
|
|
1477
|
+
if (match) {
|
|
1478
|
+
pendingTemplates.push(match[1]);
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
match = line.match(/@template ([a-zA-Z0-9_$]+)(?![a-zA-Z0-9_$])/);
|
|
1482
|
+
if (match) {
|
|
1483
|
+
pendingTemplates.push(match[1]);
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
for (let line of lines) {
|
|
1489
|
+
line = line.trim();
|
|
1490
|
+
if (line[0] === '*') {
|
|
1491
|
+
line = line.slice(1).trim();
|
|
1492
|
+
}
|
|
1493
|
+
if (line.startsWith('@template')) {
|
|
1494
|
+
harvestTemplate(line);
|
|
1495
|
+
} else if (line.startsWith('@typedef')) {
|
|
1496
|
+
const {
|
|
1497
|
+
content: def,
|
|
1498
|
+
nextIndex
|
|
1499
|
+
} = extractCurlyContent(line);
|
|
1500
|
+
let name = line.substring(nextIndex).trim();
|
|
1501
|
+
// Drop description
|
|
1502
|
+
name = name.split(' ')[0];
|
|
1503
|
+
lastTypedef = expandType(def);
|
|
1504
|
+
// Ignore @typedef's that only refer to themselves in another file (see typedef-overwrite test)
|
|
1505
|
+
if (lastTypedef !== name) {
|
|
1506
|
+
typedefs[name] = lastTypedef;
|
|
1507
|
+
if (pendingTemplates.length) {
|
|
1508
|
+
typedefTemplates[name] = [...pendingTemplates];
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
pendingTemplates = [];
|
|
1512
|
+
} else if (line.startsWith('@property')) {
|
|
1513
|
+
var _lastTypedef;
|
|
1514
|
+
// class @property
|
|
1515
|
+
if (!lastTypedef) {
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
const {
|
|
1519
|
+
content,
|
|
1520
|
+
nextIndex
|
|
1521
|
+
} = extractCurlyContent(line);
|
|
1522
|
+
const rest = line.substring(nextIndex);
|
|
1523
|
+
const propType = expandType(content);
|
|
1524
|
+
const [name, optional] = extractNameAndOptionality(rest);
|
|
1525
|
+
// console.log({name, optional, propType});
|
|
1526
|
+
const finalType = annotateOptional(propType, optional);
|
|
1527
|
+
if (((_lastTypedef = lastTypedef) == null ? void 0 : _lastTypedef.type) === 'object') {
|
|
1528
|
+
lastTypedef.properties[name] = finalType;
|
|
1529
|
+
} else {
|
|
1530
|
+
warn("not an extensible type", lastTypedef);
|
|
1531
|
+
}
|
|
1532
|
+
} else if (line.startsWith('@callback')) {
|
|
1533
|
+
const name = line.substring(9).trim();
|
|
1534
|
+
typedefs[name] = 'Function';
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
|
|
1541
|
+
*/
|
|
1542
|
+
/**
|
|
1543
|
+
* @typedef {typeof expandTypeDepFree} ExpandType
|
|
1544
|
+
* @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
|
|
1545
|
+
*/
|
|
1546
|
+
/**
|
|
1547
|
+
* Parses JSDoc comments to extract parameter type information.
|
|
1548
|
+
*
|
|
1549
|
+
* @param {string} src - The JSDoc comment string to parse.
|
|
1550
|
+
* @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
|
|
1551
|
+
* @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
|
|
1552
|
+
*/
|
|
1553
|
+
function parseJSDoc(src, expandType = expandTypeDepFree) {
|
|
1554
|
+
// Parse something like: @param {Object} [kwargs={}] Optional arguments.
|
|
1555
|
+
const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\-\{\}\.'" ]+)/g;
|
|
1556
|
+
const matches = [...src.matchAll(regex)];
|
|
1557
|
+
/** @type {Record<string, ExpandTypeReturnType>} */
|
|
1558
|
+
const params = Object.create(null);
|
|
1559
|
+
matches.forEach(_ => {
|
|
1560
|
+
const type = expandType(_[1].trim());
|
|
1561
|
+
let name = _[2].trim();
|
|
1562
|
+
let optional = false;
|
|
1563
|
+
// Examples:
|
|
1564
|
+
// name: [kwargs={}] The configuration parameters.
|
|
1565
|
+
// name: [d = 1.0] Sample spacing
|
|
1566
|
+
if (name[0] === '[') {
|
|
1567
|
+
// Counting opening/closing brackets for perfect match
|
|
1568
|
+
let openCloseCount = 1;
|
|
1569
|
+
let i = 1;
|
|
1570
|
+
for (; i < name.length; i++) {
|
|
1571
|
+
const c = name[i];
|
|
1572
|
+
if (c === '[') {
|
|
1573
|
+
openCloseCount++;
|
|
1574
|
+
} else if (c === ']') {
|
|
1575
|
+
openCloseCount--;
|
|
1576
|
+
}
|
|
1577
|
+
if (openCloseCount === 0) {
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
// Afterwards name will be: d = 1.0
|
|
1582
|
+
name = name.substring(1, i);
|
|
1583
|
+
// mark it for the type:
|
|
1584
|
+
optional = true;
|
|
1585
|
+
}
|
|
1586
|
+
// Strip the rest (either leftover of optional value or description)
|
|
1587
|
+
name = name.split(' ')[0].split('=')[0].trim();
|
|
1588
|
+
const annotatedType = annotateOptional(type, optional);
|
|
1589
|
+
// Turn "options.stats[].unitsName" into ['options', 'stats', 'unitsName'].
|
|
1590
|
+
const parts = name.split(/[\[\]]*\./);
|
|
1591
|
+
let properties = params;
|
|
1592
|
+
for (const part of parts) {
|
|
1593
|
+
const toptype = properties[part];
|
|
1594
|
+
if (!toptype) {
|
|
1595
|
+
// No toptype means we resolved as far as possible, now we can add `annotatedType`.
|
|
1596
|
+
console.assert(part === parts.at(-1), 'Current part and last part should be the same.');
|
|
1597
|
+
properties[part] = annotatedType;
|
|
1598
|
+
} else if (toptype.type === "union") {
|
|
1599
|
+
const typeObject = toptype.members.find(_ => (_ == null ? void 0 : _.type) === 'object');
|
|
1600
|
+
properties = typeObject.properties;
|
|
1601
|
+
} else if (toptype.type === "array") {
|
|
1602
|
+
properties = toptype.elementType.properties;
|
|
1603
|
+
} else if (toptype.type === "object") {
|
|
1604
|
+
toptype.properties = toptype.properties || Object.create(null);
|
|
1605
|
+
properties = toptype.properties;
|
|
1606
|
+
} else {
|
|
1607
|
+
console.warn("parseJSDoc> Skipping @param, unseen syntax detected. Please check if your JSDoc is valid or open an issue about this!", {
|
|
1608
|
+
src,
|
|
1609
|
+
toptype,
|
|
1610
|
+
parts,
|
|
1611
|
+
annotatedType
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
if (Object.keys(params).length === 0) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
return params;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Infers a parameter type from its default value AST node.
|
|
1624
|
+
* Returns widened types like TypeScript does (`= 0` means `number`, not
|
|
1625
|
+
* literal `0`; `= null` widens to `any`).
|
|
1626
|
+
* Returns `undefined` when nothing useful can be inferred — the caller then
|
|
1627
|
+
* emits no check, exactly like an undocumented parameter today.
|
|
1628
|
+
* Shapes match what `expandType` produces so they can be embedded as-is.
|
|
1629
|
+
* @param {import('@babel/types').Node} node - The default value AST node.
|
|
1630
|
+
* @returns {string | object | undefined} Inferred type or `undefined` to skip.
|
|
1631
|
+
*/
|
|
1632
|
+
function inferTypeFromDefault$1(node) {
|
|
1633
|
+
if (!node) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
switch (node.type) {
|
|
1637
|
+
case 'NumericLiteral':
|
|
1638
|
+
return 'number';
|
|
1639
|
+
case 'StringLiteral':
|
|
1640
|
+
return 'string';
|
|
1641
|
+
case 'BooleanLiteral':
|
|
1642
|
+
return 'boolean';
|
|
1643
|
+
case 'BigIntLiteral':
|
|
1644
|
+
return {
|
|
1645
|
+
type: 'bigint'
|
|
1646
|
+
};
|
|
1647
|
+
case 'RegExpLiteral':
|
|
1648
|
+
return 'RegExp';
|
|
1649
|
+
case 'TemplateLiteral':
|
|
1650
|
+
return 'string';
|
|
1651
|
+
case 'ArrayExpression':
|
|
1652
|
+
return {
|
|
1653
|
+
type: 'array',
|
|
1654
|
+
elementType: 'any'
|
|
1655
|
+
};
|
|
1656
|
+
case 'ObjectExpression':
|
|
1657
|
+
return {
|
|
1658
|
+
type: 'object',
|
|
1659
|
+
properties: {}
|
|
1660
|
+
};
|
|
1661
|
+
case 'ArrowFunctionExpression':
|
|
1662
|
+
case 'FunctionExpression':
|
|
1663
|
+
return 'Function';
|
|
1664
|
+
case 'NewExpression':
|
|
1665
|
+
{
|
|
1666
|
+
const {
|
|
1667
|
+
callee
|
|
1668
|
+
} = node;
|
|
1669
|
+
if (callee.type === 'Identifier') {
|
|
1670
|
+
return callee.name;
|
|
1671
|
+
}
|
|
1672
|
+
break;
|
|
1673
|
+
}
|
|
1674
|
+
case 'UnaryExpression':
|
|
1675
|
+
{
|
|
1676
|
+
const {
|
|
1677
|
+
operator,
|
|
1678
|
+
argument
|
|
1679
|
+
} = node;
|
|
1680
|
+
if (operator === '!') {
|
|
1681
|
+
return 'boolean';
|
|
1682
|
+
}
|
|
1683
|
+
if (operator === 'void' || operator === 'typeof') {
|
|
1684
|
+
return operator === 'void' ? 'undefined' : 'string';
|
|
1685
|
+
}
|
|
1686
|
+
if ((operator === '-' || operator === '+') && argument.type === 'NumericLiteral') {
|
|
1687
|
+
return 'number';
|
|
1688
|
+
}
|
|
1689
|
+
if ((operator === '-' || operator === '+') && argument.type === 'BigIntLiteral') {
|
|
1690
|
+
return {
|
|
1691
|
+
type: 'bigint'
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
break;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// Declaration-site ranks: the field always beats constructor assignments,
|
|
1700
|
+
// like TSC where the declared type wins over assigned values.
|
|
1701
|
+
const FIELD_JSDOC = 3;
|
|
1702
|
+
const CTOR_JSDOC = 2;
|
|
1703
|
+
const FIELD_INFER = 1;
|
|
1704
|
+
const CTOR_INFER = 0;
|
|
1705
|
+
/**
|
|
1706
|
+
* Reads the last block comment attached to a node.
|
|
1707
|
+
* @param {*} node - Babel AST node.
|
|
1708
|
+
* @returns {string|undefined} Comment text or undefined.
|
|
1709
|
+
*/
|
|
1710
|
+
function lastBlockComment(node) {
|
|
1711
|
+
const list = node == null ? void 0 : node.leadingComments;
|
|
1712
|
+
if (!list) {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
1716
|
+
if (list[i].type === 'CommentBlock') {
|
|
1717
|
+
return list[i].value;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Extracts a `{...}`-typed JSDoc tag (`@type`, `@returns`) from a comment.
|
|
1723
|
+
* @param {string|undefined} comment - Comment text.
|
|
1724
|
+
* @param {string} tag - Tag name including `@`.
|
|
1725
|
+
* @param {Function} expandType - String type to structured type.
|
|
1726
|
+
* @returns {{type: *, raw: string}|undefined} Expanded type plus raw text.
|
|
1727
|
+
*/
|
|
1728
|
+
function tagType(comment, tag, expandType) {
|
|
1729
|
+
if (!comment) {
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
const idx = comment.search(new RegExp(`${tag}(?=[\\s{])`));
|
|
1733
|
+
if (idx === -1) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
const slice = comment.slice(idx);
|
|
1737
|
+
if (!slice.includes('{')) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
const {
|
|
1742
|
+
content
|
|
1743
|
+
} = extractCurlyContent(slice);
|
|
1744
|
+
if (!content || !content.trim()) {
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
return {
|
|
1748
|
+
type: expandType(content.trim()),
|
|
1749
|
+
raw: content.trim()
|
|
1750
|
+
};
|
|
1751
|
+
} catch (_unused) {
|
|
1752
|
+
// Unparseable annotation: fail open, harvest continues.
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Reads a static property name: identifiers and string literals, including
|
|
1757
|
+
* computed `['name']` forms. Anything dynamic yields undefined.
|
|
1758
|
+
* @param {*} key - Babel key node.
|
|
1759
|
+
* @param {boolean} computed - Whether the key position is computed.
|
|
1760
|
+
* @returns {string|undefined} Property name or undefined.
|
|
1761
|
+
*/
|
|
1762
|
+
function keyName(key, computed) {
|
|
1763
|
+
if (!key) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (key.type === 'Identifier' && !computed) {
|
|
1767
|
+
return key.name;
|
|
1768
|
+
}
|
|
1769
|
+
if (key.type === 'StringLiteral') {
|
|
1770
|
+
return key.value;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Unwraps parenthesized and TS `as`/`satisfies` nodes.
|
|
1775
|
+
* @param {*} node - Babel AST node.
|
|
1776
|
+
* @returns {*} Unwrapped node.
|
|
1777
|
+
*/
|
|
1778
|
+
function unwrap(node) {
|
|
1779
|
+
while (node && (node.type === 'ParenthesizedExpression' || node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression')) {
|
|
1780
|
+
node = node.expression;
|
|
1781
|
+
}
|
|
1782
|
+
return node;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Merges a harvested entry: higher rank wins the type, flags accumulate
|
|
1786
|
+
* (`optional` reflects runtime reality, `@readonly` anywhere counts).
|
|
1787
|
+
* Two disagreeing humans (JSDoc vs JSDoc) warn, like a TSC error.
|
|
1788
|
+
* @param {Record<string, object>} fields - Collected entries by name.
|
|
1789
|
+
* @param {string} name - Property name.
|
|
1790
|
+
* @param {object} entry - Entry with type, rank, raw, readonly, optional.
|
|
1791
|
+
* @param {Function} warn - Transpile-time warning function.
|
|
1792
|
+
* @param {string} className - Class name for messages.
|
|
1793
|
+
*/
|
|
1794
|
+
function record(fields, name, entry, warn, className) {
|
|
1795
|
+
const existing = fields[name];
|
|
1796
|
+
if (!existing) {
|
|
1797
|
+
fields[name] = entry;
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (entry.jsdoc && existing.jsdoc && entry.raw !== existing.raw) {
|
|
1801
|
+
warn(`harvestClassShape: ${className}.${name} has conflicting JSDoc types (${existing.raw} vs ${entry.raw})`);
|
|
1802
|
+
}
|
|
1803
|
+
const winner = entry.rank >= existing.rank ? entry : existing;
|
|
1804
|
+
winner.readonly = existing.readonly || entry.readonly;
|
|
1805
|
+
winner.optional = existing.optional || entry.optional;
|
|
1806
|
+
fields[name] = winner;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Turns a collected entry into a type struct, applying flags.
|
|
1810
|
+
* @param {object} entry - Collected entry.
|
|
1811
|
+
* @returns {*} Type struct or name.
|
|
1812
|
+
*/
|
|
1813
|
+
function entryToType(entry) {
|
|
1814
|
+
var _entry$type;
|
|
1815
|
+
const t = (_entry$type = entry.type) != null ? _entry$type : 'any';
|
|
1816
|
+
if (!entry.readonly && !entry.optional) {
|
|
1817
|
+
return t;
|
|
1818
|
+
}
|
|
1819
|
+
if (t && typeof t === 'object') {
|
|
1820
|
+
return _extends({}, t, entry.readonly ? {
|
|
1821
|
+
readonly: true
|
|
1822
|
+
} : {}, entry.optional ? {
|
|
1823
|
+
optional: true
|
|
1824
|
+
} : {});
|
|
1825
|
+
}
|
|
1826
|
+
const out = {
|
|
1827
|
+
type: t
|
|
1828
|
+
};
|
|
1829
|
+
if (entry.readonly) {
|
|
1830
|
+
out.readonly = true;
|
|
1831
|
+
}
|
|
1832
|
+
if (entry.optional) {
|
|
1833
|
+
out.optional = true;
|
|
1834
|
+
}
|
|
1835
|
+
return out;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Records `this.x = ...` (or `+=`, `++`) assignments.
|
|
1839
|
+
* @param {*} left - Assignment target.
|
|
1840
|
+
* @param {*} right - Assigned value or undefined for op-assign/update.
|
|
1841
|
+
* @param {string|undefined} comment - Leading comment text.
|
|
1842
|
+
* @param {boolean} conditional - True inside conditionals (counts as optional).
|
|
1843
|
+
* @param {object} ctx - Harvest context with fields, expandType, warn, className.
|
|
1844
|
+
*/
|
|
1845
|
+
function recordThisAssign(left, right, comment, conditional, ctx) {
|
|
1846
|
+
var _declared$type;
|
|
1847
|
+
const {
|
|
1848
|
+
fields,
|
|
1849
|
+
expandType,
|
|
1850
|
+
warn,
|
|
1851
|
+
className
|
|
1852
|
+
} = ctx;
|
|
1853
|
+
left = unwrap(left);
|
|
1854
|
+
if (!left || left.type !== 'MemberExpression' || left.computed) {
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
if (!left.object || left.object.type !== 'ThisExpression') {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const name = keyName(left.property, false);
|
|
1861
|
+
if (name === undefined) {
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const declared = tagType(comment, '@type', expandType);
|
|
1865
|
+
if (right === undefined) {
|
|
1866
|
+
// Op-assign/update (`+=`, `++`): exists, type unknown.
|
|
1867
|
+
record(fields, name, {
|
|
1868
|
+
type: 'any',
|
|
1869
|
+
rank: CTOR_INFER,
|
|
1870
|
+
jsdoc: false,
|
|
1871
|
+
optional: conditional
|
|
1872
|
+
}, warn, className);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const inferred = inferTypeFromDefault$1(unwrap(right));
|
|
1876
|
+
const type = (_declared$type = declared == null ? void 0 : declared.type) != null ? _declared$type : inferred;
|
|
1877
|
+
if (type === undefined) {
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
record(fields, name, {
|
|
1881
|
+
type,
|
|
1882
|
+
rank: declared ? CTOR_JSDOC : CTOR_INFER,
|
|
1883
|
+
jsdoc: declared !== undefined,
|
|
1884
|
+
raw: declared == null ? void 0 : declared.raw,
|
|
1885
|
+
optional: conditional
|
|
1886
|
+
}, warn, className);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Handles `Object.assign(this, {...})` with inline object literals.
|
|
1890
|
+
* @param {*} node - CallExpression node.
|
|
1891
|
+
* @param {boolean} conditional - True inside conditionals.
|
|
1892
|
+
* @param {object} ctx - Harvest context.
|
|
1893
|
+
* @returns {boolean} True when the call was `Object.assign` on `this`.
|
|
1894
|
+
*/
|
|
1895
|
+
function recordObjectAssign(node, conditional, ctx) {
|
|
1896
|
+
var _callee$object, _callee$property;
|
|
1897
|
+
const {
|
|
1898
|
+
fields,
|
|
1899
|
+
expandType,
|
|
1900
|
+
warn,
|
|
1901
|
+
className
|
|
1902
|
+
} = ctx;
|
|
1903
|
+
const {
|
|
1904
|
+
callee,
|
|
1905
|
+
arguments: args
|
|
1906
|
+
} = node;
|
|
1907
|
+
if (!callee || callee.type !== 'MemberExpression' || callee.computed) {
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
if (((_callee$object = callee.object) == null ? void 0 : _callee$object.type) !== 'Identifier' || callee.object.name !== 'Object') {
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
if (((_callee$property = callee.property) == null ? void 0 : _callee$property.type) !== 'Identifier' || callee.property.name !== 'assign') {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
if (!args.length || args[0].type !== 'ThisExpression') {
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
for (const arg of args.slice(1)) {
|
|
1920
|
+
if (!arg || arg.type !== 'ObjectExpression') {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
for (const prop of arg.properties) {
|
|
1924
|
+
var _declared$type2;
|
|
1925
|
+
if (!prop || prop.type !== 'ObjectProperty') {
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
const name = keyName(prop.key, prop.computed);
|
|
1929
|
+
if (name === undefined) {
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
const comment = lastBlockComment(prop);
|
|
1933
|
+
const declared = tagType(comment, '@type', expandType);
|
|
1934
|
+
const type = (_declared$type2 = declared == null ? void 0 : declared.type) != null ? _declared$type2 : inferTypeFromDefault$1(unwrap(prop.value));
|
|
1935
|
+
if (type === undefined) {
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
record(fields, name, {
|
|
1939
|
+
type,
|
|
1940
|
+
rank: declared ? CTOR_JSDOC : CTOR_INFER,
|
|
1941
|
+
jsdoc: declared !== undefined,
|
|
1942
|
+
raw: declared == null ? void 0 : declared.raw,
|
|
1943
|
+
optional: conditional
|
|
1944
|
+
}, warn, className);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Walks an expression for `this.x` writes. Stops at function boundaries:
|
|
1951
|
+
* deferred writes (callbacks) are out of scope.
|
|
1952
|
+
* @param {*} expr - Babel expression node.
|
|
1953
|
+
* @param {boolean} conditional - True inside conditionals.
|
|
1954
|
+
* @param {object} ctx - Harvest context.
|
|
1955
|
+
* @param {*} stmt - Enclosing statement carrying leading comments.
|
|
1956
|
+
*/
|
|
1957
|
+
function walkExpression(expr, conditional, ctx, stmt) {
|
|
1958
|
+
if (!expr) {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
switch (expr.type) {
|
|
1962
|
+
case 'AssignmentExpression':
|
|
1963
|
+
{
|
|
1964
|
+
const comment = lastBlockComment(stmt);
|
|
1965
|
+
if (expr.operator === '=') {
|
|
1966
|
+
recordThisAssign(expr.left, expr.right, comment, conditional, ctx);
|
|
1967
|
+
walkExpression(expr.right, conditional, ctx, stmt);
|
|
1968
|
+
} else {
|
|
1969
|
+
recordThisAssign(expr.left, undefined, comment, conditional, ctx);
|
|
1970
|
+
}
|
|
1971
|
+
break;
|
|
1972
|
+
}
|
|
1973
|
+
case 'UpdateExpression':
|
|
1974
|
+
recordThisAssign(expr.argument, undefined, lastBlockComment(stmt), conditional, ctx);
|
|
1975
|
+
break;
|
|
1976
|
+
case 'SequenceExpression':
|
|
1977
|
+
for (const each of expr.expressions) {
|
|
1978
|
+
walkExpression(each, conditional, ctx, stmt);
|
|
1979
|
+
}
|
|
1980
|
+
break;
|
|
1981
|
+
case 'LogicalExpression':
|
|
1982
|
+
walkExpression(expr.right, true, ctx, stmt);
|
|
1983
|
+
break;
|
|
1984
|
+
case 'ConditionalExpression':
|
|
1985
|
+
walkExpression(expr.consequent, true, ctx, stmt);
|
|
1986
|
+
walkExpression(expr.alternate, true, ctx, stmt);
|
|
1987
|
+
break;
|
|
1988
|
+
case 'CallExpression':
|
|
1989
|
+
recordObjectAssign(expr, conditional, ctx);
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Walks a statement for `this.x` writes. Conditional wrappers mark entries
|
|
1995
|
+
* optional; nested functions are boundaries.
|
|
1996
|
+
* @param {*} st - Babel statement node.
|
|
1997
|
+
* @param {boolean} conditional - True inside conditionals.
|
|
1998
|
+
* @param {object} ctx - Harvest context.
|
|
1999
|
+
*/
|
|
2000
|
+
function walkStatement(st, conditional, ctx) {
|
|
2001
|
+
if (!st) {
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
switch (st.type) {
|
|
2005
|
+
case 'BlockStatement':
|
|
2006
|
+
for (const each of st.body) {
|
|
2007
|
+
walkStatement(each, conditional, ctx);
|
|
2008
|
+
}
|
|
2009
|
+
break;
|
|
2010
|
+
case 'ExpressionStatement':
|
|
2011
|
+
walkExpression(st.expression, conditional, ctx, st);
|
|
2012
|
+
break;
|
|
2013
|
+
case 'ReturnStatement':
|
|
2014
|
+
walkExpression(st.argument, conditional, ctx, st);
|
|
2015
|
+
break;
|
|
2016
|
+
case 'IfStatement':
|
|
2017
|
+
walkStatement(st.consequent, true, ctx);
|
|
2018
|
+
walkStatement(st.alternate, true, ctx);
|
|
2019
|
+
break;
|
|
2020
|
+
case 'WhileStatement':
|
|
2021
|
+
case 'ForStatement':
|
|
2022
|
+
case 'ForInStatement':
|
|
2023
|
+
case 'ForOfStatement':
|
|
2024
|
+
walkStatement(st.body, true, ctx);
|
|
2025
|
+
break;
|
|
2026
|
+
case 'DoWhileStatement':
|
|
2027
|
+
walkStatement(st.body, conditional, ctx);
|
|
2028
|
+
break;
|
|
2029
|
+
case 'SwitchStatement':
|
|
2030
|
+
for (const each of st.cases) {
|
|
2031
|
+
for (const consequent of each.consequent) {
|
|
2032
|
+
walkStatement(consequent, true, ctx);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
break;
|
|
2036
|
+
case 'TryStatement':
|
|
2037
|
+
walkStatement(st.block, conditional, ctx);
|
|
2038
|
+
if (st.handler) {
|
|
2039
|
+
walkStatement(st.handler.body, true, ctx);
|
|
2040
|
+
}
|
|
2041
|
+
if (st.finalizer) {
|
|
2042
|
+
walkStatement(st.finalizer, conditional, ctx);
|
|
2043
|
+
}
|
|
2044
|
+
break;
|
|
2045
|
+
case 'LabeledStatement':
|
|
2046
|
+
walkStatement(st.body, conditional, ctx);
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Reads the single parameter name of a setter.
|
|
2052
|
+
* @param {*} param - Babel parameter node.
|
|
2053
|
+
* @returns {string|undefined} Name or undefined.
|
|
2054
|
+
*/
|
|
2055
|
+
function setterParamName(param) {
|
|
2056
|
+
if (!param) {
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
if (param.type === 'Identifier') {
|
|
2060
|
+
return param.name;
|
|
2061
|
+
}
|
|
2062
|
+
if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') {
|
|
2063
|
+
return param.left.name;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Reads a setter's value type: its `@param` JSDoc first, `@type` fallback.
|
|
2068
|
+
* @param {*} set - Babel ClassMethod (kind set) node.
|
|
2069
|
+
* @param {Function} expandType - String type to structured type.
|
|
2070
|
+
* @returns {{type: *, raw: string|undefined, jsdoc: boolean}} Type plus metadata.
|
|
2071
|
+
*/
|
|
2072
|
+
function setterType(set, expandType) {
|
|
2073
|
+
const setComment = lastBlockComment(set);
|
|
2074
|
+
const paramName = setterParamName(set.params[0]);
|
|
2075
|
+
const params = parseJSDoc(setComment != null ? setComment : '', expandType);
|
|
2076
|
+
if (paramName && params && params[paramName] !== undefined) {
|
|
2077
|
+
return {
|
|
2078
|
+
type: params[paramName],
|
|
2079
|
+
raw: undefined,
|
|
2080
|
+
jsdoc: true
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
const tagged = tagType(setComment, '@type', expandType);
|
|
2084
|
+
if (tagged) {
|
|
2085
|
+
return {
|
|
2086
|
+
type: tagged.type,
|
|
2087
|
+
raw: tagged.raw,
|
|
2088
|
+
jsdoc: true
|
|
2089
|
+
};
|
|
1071
2090
|
}
|
|
1072
|
-
const content = line.substring(firstCurly + 1, k);
|
|
1073
2091
|
return {
|
|
1074
|
-
|
|
1075
|
-
|
|
2092
|
+
type: 'any',
|
|
2093
|
+
raw: undefined,
|
|
2094
|
+
jsdoc: false
|
|
1076
2095
|
};
|
|
1077
2096
|
}
|
|
1078
2097
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
* @param {
|
|
1085
|
-
* @param {
|
|
1086
|
-
* @param {
|
|
1087
|
-
* @
|
|
2098
|
+
* Harvests the instance shape of a class: field declarations (JSDoc wins,
|
|
2099
|
+
* else inferred), constructor `this.x` writes (field site wins conflicts),
|
|
2100
|
+
* methods as `Function`, getter/setter pairs as writable and getter-only as
|
|
2101
|
+
* readonly. Statics, privates and dynamic keys are skipped.
|
|
2102
|
+
* @param {*} node - Babel ClassDeclaration node.
|
|
2103
|
+
* @param {object} opts - Options with expandType and warn.
|
|
2104
|
+
* @param {Function} opts.expandType - String type to structured type.
|
|
2105
|
+
* @param {Function} opts.warn - Transpile-time warning function.
|
|
2106
|
+
* @returns {{name: string, shape: object}|undefined} Class name plus object shape.
|
|
1088
2107
|
*/
|
|
1089
|
-
function
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (type !== '
|
|
2108
|
+
function harvestClassShape(node, {
|
|
2109
|
+
expandType,
|
|
2110
|
+
warn
|
|
2111
|
+
}) {
|
|
2112
|
+
const id = node == null ? void 0 : node.id;
|
|
2113
|
+
if (!id || id.type !== 'Identifier' || !id.name) {
|
|
1095
2114
|
return;
|
|
1096
2115
|
}
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
2116
|
+
const className = id.name;
|
|
2117
|
+
const ctx = {
|
|
2118
|
+
fields: {},
|
|
2119
|
+
gets: {},
|
|
2120
|
+
sets: {},
|
|
2121
|
+
expandType,
|
|
2122
|
+
warn,
|
|
2123
|
+
className
|
|
2124
|
+
};
|
|
2125
|
+
let ctor = null;
|
|
2126
|
+
for (const el of node.body.body) {
|
|
2127
|
+
if (el.type === 'ClassPrivateProperty' || el.type === 'ClassPrivateMethod') {
|
|
2128
|
+
continue;
|
|
1103
2129
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
lastTypedef = expandType(def);
|
|
1113
|
-
// Ignore @typedef's that only refer to themselves in another file (see typedef-overwrite test)
|
|
1114
|
-
if (lastTypedef !== name) {
|
|
1115
|
-
typedefs[name] = lastTypedef;
|
|
2130
|
+
const name = keyName(el.key, el.computed);
|
|
2131
|
+
if (name === undefined) {
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
if (el.type === 'ClassProperty' || el.type === 'PropertyDefinition') {
|
|
2135
|
+
var _declared$type3;
|
|
2136
|
+
if (el.static || el.declare) {
|
|
2137
|
+
continue;
|
|
1116
2138
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2139
|
+
const comment = lastBlockComment(el);
|
|
2140
|
+
const declared = tagType(comment, '@type', expandType);
|
|
2141
|
+
const inferred = el.value ? inferTypeFromDefault$1(unwrap(el.value)) : undefined;
|
|
2142
|
+
const type = (_declared$type3 = declared == null ? void 0 : declared.type) != null ? _declared$type3 : inferred;
|
|
2143
|
+
if (type === undefined) {
|
|
1121
2144
|
continue;
|
|
1122
2145
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
} else {
|
|
1135
|
-
warn("not an extensible type", lastTypedef);
|
|
2146
|
+
record(ctx.fields, name, {
|
|
2147
|
+
type,
|
|
2148
|
+
rank: declared ? FIELD_JSDOC : FIELD_INFER,
|
|
2149
|
+
jsdoc: declared !== undefined,
|
|
2150
|
+
raw: declared == null ? void 0 : declared.raw,
|
|
2151
|
+
readonly: el.readonly === true || (comment ? /@readonly(?![\w])/.test(comment) : false),
|
|
2152
|
+
optional: el.optional === true
|
|
2153
|
+
}, warn, className);
|
|
2154
|
+
} else if (el.type === 'ClassMethod' || el.type === 'ClassPrivateMethod') {
|
|
2155
|
+
if (el.static) {
|
|
2156
|
+
continue;
|
|
1136
2157
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
2158
|
+
if (el.kind === 'constructor') {
|
|
2159
|
+
ctor = el;
|
|
2160
|
+
} else if (el.kind === 'method') {
|
|
2161
|
+
var _ctx$fields$name;
|
|
2162
|
+
ctx.fields[name] = (_ctx$fields$name = ctx.fields[name]) != null ? _ctx$fields$name : {
|
|
2163
|
+
type: 'Function',
|
|
2164
|
+
rank: FIELD_INFER,
|
|
2165
|
+
jsdoc: false
|
|
2166
|
+
};
|
|
2167
|
+
} else if (el.kind === 'get') {
|
|
2168
|
+
ctx.gets[name] = el;
|
|
2169
|
+
} else if (el.kind === 'set') {
|
|
2170
|
+
ctx.sets[name] = el;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
for (const name of Object.keys(ctx.gets)) {
|
|
2175
|
+
var _tagType, _ref, _getType$type, _getType$raw;
|
|
2176
|
+
const get = ctx.gets[name];
|
|
2177
|
+
const set = ctx.sets[name];
|
|
2178
|
+
const getComment = lastBlockComment(get);
|
|
2179
|
+
const getType = (_tagType = tagType(getComment, '@type', expandType)) != null ? _tagType : tagType(getComment, '@returns', expandType);
|
|
2180
|
+
const setInfo = set ? setterType(set, expandType) : undefined;
|
|
2181
|
+
const type = (_ref = (_getType$type = getType == null ? void 0 : getType.type) != null ? _getType$type : setInfo == null ? void 0 : setInfo.type) != null ? _ref : 'any';
|
|
2182
|
+
record(ctx.fields, name, {
|
|
2183
|
+
type,
|
|
2184
|
+
rank: getType || setInfo != null && setInfo.jsdoc ? FIELD_JSDOC : FIELD_INFER,
|
|
2185
|
+
jsdoc: Boolean(getType || (setInfo == null ? void 0 : setInfo.jsdoc)),
|
|
2186
|
+
raw: (_getType$raw = getType == null ? void 0 : getType.raw) != null ? _getType$raw : setInfo == null ? void 0 : setInfo.raw,
|
|
2187
|
+
readonly: !set
|
|
2188
|
+
}, warn, className);
|
|
2189
|
+
}
|
|
2190
|
+
for (const name of Object.keys(ctx.sets)) {
|
|
2191
|
+
if (ctx.gets[name]) {
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
const setInfo = setterType(ctx.sets[name], expandType);
|
|
2195
|
+
record(ctx.fields, name, {
|
|
2196
|
+
type: setInfo.type,
|
|
2197
|
+
rank: setInfo.jsdoc ? FIELD_JSDOC : FIELD_INFER,
|
|
2198
|
+
jsdoc: setInfo.jsdoc,
|
|
2199
|
+
raw: setInfo.raw
|
|
2200
|
+
}, warn, className);
|
|
2201
|
+
}
|
|
2202
|
+
if (ctor && ctor.body) {
|
|
2203
|
+
for (const st of ctor.body.body) {
|
|
2204
|
+
walkStatement(st, false, ctx);
|
|
1140
2205
|
}
|
|
1141
2206
|
}
|
|
2207
|
+
const properties = {};
|
|
2208
|
+
for (const name of Object.keys(ctx.fields)) {
|
|
2209
|
+
properties[name] = entryToType(ctx.fields[name]);
|
|
2210
|
+
}
|
|
2211
|
+
if (!Object.keys(properties).length) {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
return {
|
|
2215
|
+
name: className,
|
|
2216
|
+
shape: {
|
|
2217
|
+
type: 'object',
|
|
2218
|
+
properties
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
1142
2221
|
}
|
|
1143
2222
|
|
|
1144
2223
|
/**
|
|
@@ -1190,89 +2269,6 @@ function nodeIsFunctionLike(node) {
|
|
|
1190
2269
|
return false;
|
|
1191
2270
|
}
|
|
1192
2271
|
|
|
1193
|
-
/**
|
|
1194
|
-
* @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
|
|
1195
|
-
*/
|
|
1196
|
-
/**
|
|
1197
|
-
* @typedef {typeof expandTypeDepFree} ExpandType
|
|
1198
|
-
* @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
|
|
1199
|
-
*/
|
|
1200
|
-
/**
|
|
1201
|
-
* Parses JSDoc comments to extract parameter type information.
|
|
1202
|
-
*
|
|
1203
|
-
* @param {string} src - The JSDoc comment string to parse.
|
|
1204
|
-
* @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
|
|
1205
|
-
* @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
|
|
1206
|
-
*/
|
|
1207
|
-
function parseJSDoc(src, expandType = expandTypeDepFree) {
|
|
1208
|
-
// Parse something like: @param {Object} [kwargs={}] Optional arguments.
|
|
1209
|
-
const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\-\{\}\.'" ]+)/g;
|
|
1210
|
-
const matches = [...src.matchAll(regex)];
|
|
1211
|
-
/** @type {Record<string, ExpandTypeReturnType>} */
|
|
1212
|
-
const params = Object.create(null);
|
|
1213
|
-
matches.forEach(_ => {
|
|
1214
|
-
const type = expandType(_[1].trim());
|
|
1215
|
-
let name = _[2].trim();
|
|
1216
|
-
let optional = false;
|
|
1217
|
-
// Examples:
|
|
1218
|
-
// name: [kwargs={}] The configuration parameters.
|
|
1219
|
-
// name: [d = 1.0] Sample spacing
|
|
1220
|
-
if (name[0] === '[') {
|
|
1221
|
-
// Counting opening/closing brackets for perfect match
|
|
1222
|
-
let openCloseCount = 1;
|
|
1223
|
-
let i = 1;
|
|
1224
|
-
for (; i < name.length; i++) {
|
|
1225
|
-
const c = name[i];
|
|
1226
|
-
if (c === '[') {
|
|
1227
|
-
openCloseCount++;
|
|
1228
|
-
} else if (c === ']') {
|
|
1229
|
-
openCloseCount--;
|
|
1230
|
-
}
|
|
1231
|
-
if (openCloseCount === 0) {
|
|
1232
|
-
break;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
// Afterwards name will be: d = 1.0
|
|
1236
|
-
name = name.substring(1, i);
|
|
1237
|
-
// mark it for the type:
|
|
1238
|
-
optional = true;
|
|
1239
|
-
}
|
|
1240
|
-
// Strip the rest (either leftover of optional value or description)
|
|
1241
|
-
name = name.split(' ')[0].split('=')[0].trim();
|
|
1242
|
-
const annotatedType = annotateOptional(type, optional);
|
|
1243
|
-
// Turn "options.stats[].unitsName" into ['options', 'stats', 'unitsName'].
|
|
1244
|
-
const parts = name.split(/[\[\]]*\./);
|
|
1245
|
-
let properties = params;
|
|
1246
|
-
for (const part of parts) {
|
|
1247
|
-
const toptype = properties[part];
|
|
1248
|
-
if (!toptype) {
|
|
1249
|
-
// No toptype means we resolved as far as possible, now we can add `annotatedType`.
|
|
1250
|
-
console.assert(part === parts.at(-1), 'Current part and last part should be the same.');
|
|
1251
|
-
properties[part] = annotatedType;
|
|
1252
|
-
} else if (toptype.type === "union") {
|
|
1253
|
-
const typeObject = toptype.members.find(_ => (_ == null ? void 0 : _.type) === 'object');
|
|
1254
|
-
properties = typeObject.properties;
|
|
1255
|
-
} else if (toptype.type === "array") {
|
|
1256
|
-
properties = toptype.elementType.properties;
|
|
1257
|
-
} else if (toptype.type === "object") {
|
|
1258
|
-
toptype.properties = toptype.properties || Object.create(null);
|
|
1259
|
-
properties = toptype.properties;
|
|
1260
|
-
} else {
|
|
1261
|
-
console.warn("parseJSDoc> Skipping @param, unseen syntax detected. Please check if your JSDoc is valid or open an issue about this!", {
|
|
1262
|
-
src,
|
|
1263
|
-
toptype,
|
|
1264
|
-
parts,
|
|
1265
|
-
annotatedType
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
});
|
|
1270
|
-
if (Object.keys(params).length === 0) {
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
return params;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
2272
|
/**
|
|
1277
2273
|
* @param {string} src - JSDoc comment of the setter.
|
|
1278
2274
|
* @param {Function} expandType - The expandType function.
|
|
@@ -1305,9 +2301,6 @@ function parseJSDocSetter(src, expandType = expandTypeDepFree) {
|
|
|
1305
2301
|
function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
|
|
1306
2302
|
const regexTemplateTyped = /@template \{(.*?)\} ([a-zA-Z0-9_$=]+)/g;
|
|
1307
2303
|
const matches = [...src.matchAll(regexTemplateTyped)];
|
|
1308
|
-
if (!matches.length) {
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
2304
|
/** @type {Record<string, ExpandTypeReturnType>} */
|
|
1312
2305
|
const templates = Object.create(null);
|
|
1313
2306
|
matches.forEach(_ => {
|
|
@@ -1315,22 +2308,23 @@ function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
|
|
|
1315
2308
|
const name = _[2].trim();
|
|
1316
2309
|
templates[name] = type;
|
|
1317
2310
|
});
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
2311
|
+
// `@template [A=X]` defaults: constraint X under name A.
|
|
2312
|
+
const regexTemplateDefault = /@template \[([a-zA-Z0-9_$]+)=([^\]]+)\]/g;
|
|
2313
|
+
for (const match of src.matchAll(regexTemplateDefault)) {
|
|
2314
|
+
templates[match[1]] = expandType(match[2].trim());
|
|
2315
|
+
}
|
|
2316
|
+
// Bare `@template T`: unconstrained, stands in as any.
|
|
2317
|
+
const regexTemplateBare = /@template ([a-zA-Z0-9_$]+)(?![a-zA-Z0-9_$])/g;
|
|
2318
|
+
for (const match of src.matchAll(regexTemplateBare)) {
|
|
2319
|
+
const name = match[1];
|
|
2320
|
+
if (!(name in templates)) {
|
|
2321
|
+
templates[name] = 'any';
|
|
1330
2322
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2323
|
+
}
|
|
2324
|
+
if (!Object.keys(templates).length) {
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
return templates;
|
|
1334
2328
|
}
|
|
1335
2329
|
|
|
1336
2330
|
/**
|
|
@@ -1355,8 +2349,12 @@ function mapValues(obj, fn) {
|
|
|
1355
2349
|
*/
|
|
1356
2350
|
/**
|
|
1357
2351
|
* Recursively clones and simplifies a type for emission into source code.
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
2352
|
+
* Empty `properties` are preserved (not deleted) and empty `object` types
|
|
2353
|
+
* are kept structured: `{type: 'object'}` without a `properties` key is the
|
|
2354
|
+
* `{}` literal (accepts every non-nullish value like TS), while
|
|
2355
|
+
* `{type: 'object', properties: {}}` is the `object` keyword (rejects
|
|
2356
|
+
* primitives). Collapsing either form would erase that distinction.
|
|
2357
|
+
* Numbers/booleans (literal types) pass through.
|
|
1360
2358
|
* Non-destructive — the original type tree is never mutated.
|
|
1361
2359
|
* @param {string | DocType | number | boolean} type - The type.
|
|
1362
2360
|
* @returns {string | DocType | number | boolean} The simplified type.
|
|
@@ -1368,9 +2366,6 @@ function simplifyType(type) {
|
|
|
1368
2366
|
const out = _extends({}, type);
|
|
1369
2367
|
if (out.properties) {
|
|
1370
2368
|
out.properties = mapValues(out.properties, simplifyType);
|
|
1371
|
-
if (out.type === 'object' && !Object.keys(out.properties).length) {
|
|
1372
|
-
delete out.properties;
|
|
1373
|
-
}
|
|
1374
2369
|
}
|
|
1375
2370
|
if (out.indexSignatures && Array.isArray(out.indexSignatures)) {
|
|
1376
2371
|
out.indexSignatures = out.indexSignatures.map(simplifyType);
|
|
@@ -1397,9 +2392,6 @@ function simplifyType(type) {
|
|
|
1397
2392
|
if (out.type === 'typeof' && out.argument) {
|
|
1398
2393
|
out.argument = simplifyType(out.argument);
|
|
1399
2394
|
}
|
|
1400
|
-
if (out.type === 'object' && !out.properties && !out.indexSignatures && !out.optional) {
|
|
1401
|
-
return 'object';
|
|
1402
|
-
}
|
|
1403
2395
|
return out;
|
|
1404
2396
|
}
|
|
1405
2397
|
|
|
@@ -3587,6 +4579,8 @@ class Asserter extends Stringifier {
|
|
|
3587
4579
|
};
|
|
3588
4580
|
/** @type {Record<string, object>} */
|
|
3589
4581
|
this.typedefs = {};
|
|
4582
|
+
/** @type {Record<string, string[]>} */
|
|
4583
|
+
this.typedefTemplates = {};
|
|
3590
4584
|
/** @type {string[]} */
|
|
3591
4585
|
this.addLaterImportNamespaceSpecifier = [];
|
|
3592
4586
|
this.forceCurly = forceCurly;
|
|
@@ -3649,6 +4643,21 @@ class Asserter extends Stringifier {
|
|
|
3649
4643
|
const id_ = this.toSource(id);
|
|
3650
4644
|
let out = super.ClassDeclaration(node);
|
|
3651
4645
|
out += `${this.spaces}registerClass(${id_});`;
|
|
4646
|
+
const harvested = harvestClassShape(node, {
|
|
4647
|
+
expandType: this.expandType,
|
|
4648
|
+
warn: this.warn.bind(this)
|
|
4649
|
+
});
|
|
4650
|
+
if (harvested) {
|
|
4651
|
+
// Hand-written typedefs win: emitting a second registerTypedef for the
|
|
4652
|
+
// same name would be last-wins deterministic but noisy, so the harvest
|
|
4653
|
+
// step skips names already present in this.typedefs (populated in File).
|
|
4654
|
+
if (this.typedefs[harvested.name]) {
|
|
4655
|
+
this.warn(`harvestClassShape: skipping harvested shape for '${harvested.name}', hand-written typedef wins`);
|
|
4656
|
+
} else {
|
|
4657
|
+
const json = simplifyTypeToSource(harvested.shape);
|
|
4658
|
+
out += `\n${this.spaces}registerTypedef('${harvested.name}', ${json});`;
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
3652
4661
|
return out;
|
|
3653
4662
|
}
|
|
3654
4663
|
/**
|
|
@@ -4496,7 +5505,7 @@ class Asserter extends Stringifier {
|
|
|
4496
5505
|
if (comments) {
|
|
4497
5506
|
for (const comment of comments) {
|
|
4498
5507
|
const warn = this.warn.bind(this);
|
|
4499
|
-
parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
|
|
5508
|
+
parseJSDocTypedef(this.typedefs, this.typedefTemplates, warn, comment, this.expandType);
|
|
4500
5509
|
}
|
|
4501
5510
|
}
|
|
4502
5511
|
//console.log("this.typedefs", this.typedefs);
|
|
@@ -4504,7 +5513,8 @@ class Asserter extends Stringifier {
|
|
|
4504
5513
|
for (const name in this.typedefs) {
|
|
4505
5514
|
const typedef = this.typedefs[name];
|
|
4506
5515
|
const json = simplifyTypeToSource(typedef);
|
|
4507
|
-
|
|
5516
|
+
const params = this.typedefTemplates[name];
|
|
5517
|
+
out += params != null && params.length ? `registerTypedef('${name}', ${json}, ${JSON.stringify(params)});\n` : `registerTypedef('${name}', ${json});\n`;
|
|
4508
5518
|
}
|
|
4509
5519
|
const code = this.toSource(program) + '\n';
|
|
4510
5520
|
out += code;
|
|
@@ -4774,7 +5784,13 @@ function toSourceBabelTS(node) {
|
|
|
4774
5784
|
{
|
|
4775
5785
|
const name = toSourceBabelTS(node.typeName);
|
|
4776
5786
|
if (!node.typeParameters) {
|
|
4777
|
-
//
|
|
5787
|
+
// Bare `Object` is the empty object type, matching expandType().
|
|
5788
|
+
if (name === 'Object') {
|
|
5789
|
+
return {
|
|
5790
|
+
type: 'object',
|
|
5791
|
+
properties: {}
|
|
5792
|
+
};
|
|
5793
|
+
}
|
|
4778
5794
|
// Bare reference: Identifier gives name, TSQualifiedName gives dotted path.
|
|
4779
5795
|
return name;
|
|
4780
5796
|
}
|
|
@@ -4835,6 +5851,8 @@ function toSourceBabelTS(node) {
|
|
|
4835
5851
|
}
|
|
4836
5852
|
case 'TSStringKeyword':
|
|
4837
5853
|
return 'string';
|
|
5854
|
+
case 'TSSymbolKeyword':
|
|
5855
|
+
return 'symbol';
|
|
4838
5856
|
case 'TSNumberKeyword':
|
|
4839
5857
|
return 'number';
|
|
4840
5858
|
case 'TSIntersectionType':
|
|
@@ -4916,6 +5934,8 @@ function toSourceBabelTS(node) {
|
|
|
4916
5934
|
case 'ParenthesizedType':
|
|
4917
5935
|
// fall-through for parentheses
|
|
4918
5936
|
return toSourceBabelTS(node.type);
|
|
5937
|
+
case 'TSParenthesizedType':
|
|
5938
|
+
return toSourceBabelTS(node.typeAnnotation);
|
|
4919
5939
|
case 'LastTypeNode':
|
|
4920
5940
|
return toSourceBabelTS(node.qualifier);
|
|
4921
5941
|
case 'TSTypeQuery':
|
|
@@ -4924,11 +5944,120 @@ function toSourceBabelTS(node) {
|
|
|
4924
5944
|
type: 'typeof',
|
|
4925
5945
|
argument
|
|
4926
5946
|
};
|
|
5947
|
+
case 'TSConditionalType':
|
|
5948
|
+
{
|
|
5949
|
+
const checkType = toSourceBabelTS(node.checkType);
|
|
5950
|
+
const extendsType = toSourceBabelTS(node.extendsType);
|
|
5951
|
+
const trueType = toSourceBabelTS(node.trueType);
|
|
5952
|
+
const falseType = toSourceBabelTS(node.falseType);
|
|
5953
|
+
return {
|
|
5954
|
+
type: 'condition',
|
|
5955
|
+
checkType,
|
|
5956
|
+
extendsType,
|
|
5957
|
+
trueType,
|
|
5958
|
+
falseType
|
|
5959
|
+
};
|
|
5960
|
+
}
|
|
5961
|
+
case 'TSIndexedAccessType':
|
|
5962
|
+
{
|
|
5963
|
+
const index = toSourceBabelTS(node.indexType);
|
|
5964
|
+
const object = toSourceBabelTS(node.objectType);
|
|
5965
|
+
return {
|
|
5966
|
+
type: 'indexedAccess',
|
|
5967
|
+
index,
|
|
5968
|
+
object
|
|
5969
|
+
};
|
|
5970
|
+
}
|
|
5971
|
+
case 'TSMappedType':
|
|
5972
|
+
{
|
|
5973
|
+
const param = node.typeParameter;
|
|
5974
|
+
const nameNode = param == null ? void 0 : param.name;
|
|
5975
|
+
const element = typeof nameNode === 'string' ? nameNode : toSourceBabelTS(nameNode);
|
|
5976
|
+
const iterable = toSourceBabelTS(param == null ? void 0 : param.constraint);
|
|
5977
|
+
const result = toSourceBabelTS(node.typeAnnotation);
|
|
5978
|
+
const out = {
|
|
5979
|
+
type: 'mapping',
|
|
5980
|
+
iterable,
|
|
5981
|
+
element,
|
|
5982
|
+
result
|
|
5983
|
+
};
|
|
5984
|
+
if (node.nameType) {
|
|
5985
|
+
out.nameType = toSourceBabelTS(node.nameType);
|
|
5986
|
+
}
|
|
5987
|
+
if (node.optional !== undefined && node.optional !== false && node.optional !== null) {
|
|
5988
|
+
out.question = node.optional === '-' ? '-' : node.optional === '+' ? '+' : '?';
|
|
5989
|
+
}
|
|
5990
|
+
if (node.readonly !== undefined && node.readonly !== false && node.readonly !== null) {
|
|
5991
|
+
out.readonly = node.readonly === '-' ? '-' : node.readonly === '+' ? '+' : 'readonly';
|
|
5992
|
+
}
|
|
5993
|
+
return out;
|
|
5994
|
+
}
|
|
5995
|
+
case 'TSTypeAnnotation':
|
|
5996
|
+
return toSourceBabelTS(node.typeAnnotation);
|
|
5997
|
+
case 'TSTypeLiteral':
|
|
5998
|
+
{
|
|
5999
|
+
const _properties = {};
|
|
6000
|
+
let indexSignatures;
|
|
6001
|
+
for (const member of (_node$members = node.members) != null ? _node$members : []) {
|
|
6002
|
+
var _node$members;
|
|
6003
|
+
if (member.type === 'TSIndexSignature') {
|
|
6004
|
+
var _indexSignatures;
|
|
6005
|
+
indexSignatures = (_indexSignatures = indexSignatures) != null ? _indexSignatures : [];
|
|
6006
|
+
indexSignatures.push(toSourceBabelTS(member));
|
|
6007
|
+
} else if (member.type === 'TSPropertySignature') {
|
|
6008
|
+
const name = toSourceBabelTS(member.key);
|
|
6009
|
+
let type = toSourceBabelTS(member.typeAnnotation);
|
|
6010
|
+
if (member.optional) {
|
|
6011
|
+
if (type && typeof type === 'object') type.optional = true;else type = {
|
|
6012
|
+
type,
|
|
6013
|
+
optional: true
|
|
6014
|
+
};
|
|
6015
|
+
}
|
|
6016
|
+
if (member.readonly) {
|
|
6017
|
+
if (type && typeof type === 'object') type.readonly = true;else type = {
|
|
6018
|
+
type,
|
|
6019
|
+
readonly: true
|
|
6020
|
+
};
|
|
6021
|
+
}
|
|
6022
|
+
_properties[name] = type;
|
|
6023
|
+
} else {
|
|
6024
|
+
console.warn('TSTypeLiteral: unhandled member', member.type);
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
const ret = {
|
|
6028
|
+
type: 'object'
|
|
6029
|
+
};
|
|
6030
|
+
if (Object.keys(_properties).length) {
|
|
6031
|
+
ret.properties = _properties;
|
|
6032
|
+
}
|
|
6033
|
+
if (indexSignatures) {
|
|
6034
|
+
ret.indexSignatures = indexSignatures;
|
|
6035
|
+
}
|
|
6036
|
+
return ret;
|
|
6037
|
+
}
|
|
6038
|
+
case 'TSIndexSignature':
|
|
6039
|
+
{
|
|
6040
|
+
var _node$parameters;
|
|
6041
|
+
const indexType = toSourceBabelTS(node.typeAnnotation);
|
|
6042
|
+
const indexParameters = ((_node$parameters = node.parameters) != null ? _node$parameters : []).map(toSourceBabelTS);
|
|
6043
|
+
return {
|
|
6044
|
+
type: 'indexSignature',
|
|
6045
|
+
indexType,
|
|
6046
|
+
indexParameters
|
|
6047
|
+
};
|
|
6048
|
+
}
|
|
4927
6049
|
case 'TSTypeOperator':
|
|
4928
6050
|
if (node.operator === 'readonly') {
|
|
4929
6051
|
// readonly erased at runtime, same shape as the inner type.
|
|
4930
6052
|
return toSourceBabelTS(node.typeAnnotation);
|
|
4931
6053
|
}
|
|
6054
|
+
if (node.operator === 'keyof') {
|
|
6055
|
+
const keyofArg = toSourceBabelTS(node.typeAnnotation);
|
|
6056
|
+
return {
|
|
6057
|
+
type: 'keyof',
|
|
6058
|
+
argument: keyofArg
|
|
6059
|
+
};
|
|
6060
|
+
}
|
|
4932
6061
|
console.warn('unimplemented TSTypeOperator', node.operator);
|
|
4933
6062
|
return 'any';
|
|
4934
6063
|
case 'TSQualifiedName':
|
|
@@ -5059,6 +6188,8 @@ class JSDocAnnotator {
|
|
|
5059
6188
|
this.parents = [];
|
|
5060
6189
|
/** @type {Record<string, object>} */
|
|
5061
6190
|
this.typedefs = {};
|
|
6191
|
+
/** @type {Record<string, string[]>} */
|
|
6192
|
+
this.typedefTemplates = {};
|
|
5062
6193
|
/** @type {import('./parseJSDoc.js').ExpandType} */
|
|
5063
6194
|
this.expandType = options.expandType || expandTypeDepFree;
|
|
5064
6195
|
}
|
|
@@ -5075,7 +6206,7 @@ class JSDocAnnotator {
|
|
|
5075
6206
|
if (comments) {
|
|
5076
6207
|
for (const comment of comments) {
|
|
5077
6208
|
const warn = console.warn.bind(console);
|
|
5078
|
-
parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
|
|
6209
|
+
parseJSDocTypedef(this.typedefs, this.typedefTemplates, warn, comment, this.expandType);
|
|
5079
6210
|
}
|
|
5080
6211
|
}
|
|
5081
6212
|
this.traverse(ast.program);
|