@runtime-type-inspector/transpiler 5.0.0 → 5.0.2
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 +1551 -282
- package/index.mjs +1450 -279
- 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,89 @@ 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
|
+
}
|
|
883
1268
|
return {
|
|
884
1269
|
type: 'object',
|
|
885
1270
|
properties
|
|
886
1271
|
};
|
|
887
1272
|
}
|
|
888
|
-
// (5)
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1273
|
+
// (5) Unions and intersections via top-level splits (`&` binds tighter, so `|` first).
|
|
1274
|
+
// Note: conditionals already handled up front (before nullable) so `D?`
|
|
1275
|
+
// false branches keep their nullability; this slot is intentionally union-only.
|
|
1276
|
+
const unionParts = splitTopLevel(type, "|");
|
|
1277
|
+
if (unionParts.length >= 2) {
|
|
892
1278
|
return {
|
|
893
1279
|
type: 'union',
|
|
894
|
-
members:
|
|
1280
|
+
members: unionParts.map(_ => expandTypeDepFree(_.trim()))
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const interParts = splitTopLevel(type, "&");
|
|
1284
|
+
if (interParts.length >= 2) {
|
|
1285
|
+
return {
|
|
1286
|
+
type: 'intersection',
|
|
1287
|
+
members: interParts.map(_ => expandTypeDepFree(_.trim()))
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
// (7) `keyof T` before typeof so `keyof typeof X` nests correctly.
|
|
1291
|
+
if (type.startsWith('keyof ') || type.startsWith('keyof(')) {
|
|
1292
|
+
const after = type.startsWith('keyof(') ? type.slice(5).trim() : type.slice(6).trim();
|
|
1293
|
+
if (after) {
|
|
1294
|
+
return {
|
|
1295
|
+
type: 'keyof',
|
|
1296
|
+
argument: expandTypeDepFree(after)
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// (8) Indexed access `T[K]` (arrays `T[]` handled below, tuples above start with `[`).
|
|
1301
|
+
const indexed = parseIndexedAccessRaw(type);
|
|
1302
|
+
if (indexed) {
|
|
1303
|
+
return {
|
|
1304
|
+
type: 'indexedAccess',
|
|
1305
|
+
index: expandTypeDepFree(indexed.indexRaw),
|
|
1306
|
+
object: expandTypeDepFree(indexed.objectRaw)
|
|
895
1307
|
};
|
|
896
1308
|
}
|
|
897
|
-
// (
|
|
1309
|
+
// (9) expand [] Arrays
|
|
898
1310
|
// Test arrays: new pc.Mat3().set([1, 2, 3, "asd"])
|
|
899
1311
|
if (type.endsWith("[]")) {
|
|
900
1312
|
const typeSlice = type.slice(0, -2);
|
|
@@ -903,17 +1315,24 @@ function expandTypeDepFree(type) {
|
|
|
903
1315
|
elementType: expandTypeDepFree(typeSlice)
|
|
904
1316
|
};
|
|
905
1317
|
}
|
|
906
|
-
// (
|
|
1318
|
+
// (10) expand tuples
|
|
907
1319
|
if (type[0] === '[' && type[type.length - 1] === ']') {
|
|
908
|
-
const
|
|
1320
|
+
const inner = type.slice(1, -1);
|
|
1321
|
+
if (inner.trim() === '') {
|
|
1322
|
+
return {
|
|
1323
|
+
type: 'tuple',
|
|
1324
|
+
elements: []
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const elements = splitTopLevel(inner, ','); // ['null', ' Texture', ...]
|
|
909
1328
|
return {
|
|
910
1329
|
type: 'tuple',
|
|
911
|
-
elements: elements.map(expandTypeDepFree)
|
|
1330
|
+
elements: elements.map(_ => expandTypeDepFree(_.trim()))
|
|
912
1331
|
};
|
|
913
1332
|
}
|
|
914
|
-
// (
|
|
1333
|
+
// (11) expand typeof expressions
|
|
915
1334
|
if (type.startsWith('typeof ')) {
|
|
916
|
-
const argument = expandTypeDepFree(type.substring(7));
|
|
1335
|
+
const argument = expandTypeDepFree(type.substring(7).trim());
|
|
917
1336
|
return {
|
|
918
1337
|
type: 'typeof',
|
|
919
1338
|
argument
|
|
@@ -939,81 +1358,19 @@ function expandTypeDepFree(type) {
|
|
|
939
1358
|
return type;
|
|
940
1359
|
}
|
|
941
1360
|
|
|
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
|
-
};
|
|
1361
|
+
function _extends() {
|
|
1362
|
+
_extends = Object.assign ? Object.assign.bind() : function (target) {
|
|
1363
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
1364
|
+
var source = arguments[i];
|
|
1365
|
+
for (var key in source) {
|
|
1366
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1367
|
+
target[key] = source[key];
|
|
1013
1368
|
}
|
|
1014
|
-
break;
|
|
1015
1369
|
}
|
|
1016
|
-
|
|
1370
|
+
}
|
|
1371
|
+
return target;
|
|
1372
|
+
};
|
|
1373
|
+
return _extends.apply(this, arguments);
|
|
1017
1374
|
}
|
|
1018
1375
|
|
|
1019
1376
|
/**
|
|
@@ -1069,76 +1426,791 @@ function extractCurlyContent(line) {
|
|
|
1069
1426
|
break;
|
|
1070
1427
|
}
|
|
1071
1428
|
}
|
|
1072
|
-
const content = line.substring(firstCurly + 1, k);
|
|
1429
|
+
const content = line.substring(firstCurly + 1, k);
|
|
1430
|
+
return {
|
|
1431
|
+
content,
|
|
1432
|
+
nextIndex: k + 1
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Parses JSDoc comments to extract and expand typedefs and their associated properties.
|
|
1437
|
+
*
|
|
1438
|
+
* It iterates through the lines of a `CommentBlock` from the Babel AST, looking for `@typedef` and `@property`
|
|
1439
|
+
* annotations. When it finds a typedef, it stores it in the `typedefs` record. When it finds a property,
|
|
1440
|
+
* it adds it to the last found typedef if it is an object type. `@template` names preceding a
|
|
1441
|
+
* typedef are recorded in `typedefTemplates` so generic references can instantiate.
|
|
1442
|
+
* @param {Record<string, object>} typedefs - An object to store typedefs, mapping type names to their expanded definitions.
|
|
1443
|
+
* @param {Record<string, string[]>} typedefTemplates - An object to store template parameter names per generic typedef.
|
|
1444
|
+
* @param {Console["warn"]} warn - A warn function used for emitting warnings about non-extensible types.
|
|
1445
|
+
* @param {import("@babel/types").Comment} comment - A comment extracted from Babel's AST, expected to be a CommentBlock containing type definitions.
|
|
1446
|
+
* @param {Function} expandType - A function that takes a type expression as a string and returns a structured representation of the type.
|
|
1447
|
+
*/
|
|
1448
|
+
function parseJSDocTypedef(typedefs, typedefTemplates, warn, comment, expandType) {
|
|
1449
|
+
const {
|
|
1450
|
+
type,
|
|
1451
|
+
value
|
|
1452
|
+
} = comment;
|
|
1453
|
+
if (type !== 'CommentBlock') {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const lines = value.split('\n');
|
|
1457
|
+
let lastTypedef;
|
|
1458
|
+
let pendingTemplates = [];
|
|
1459
|
+
/**
|
|
1460
|
+
* @param {string} line - The trimmed JSDoc line.
|
|
1461
|
+
* @returns {boolean} True when the line held a template tag.
|
|
1462
|
+
*/
|
|
1463
|
+
function harvestTemplate(line) {
|
|
1464
|
+
let match = line.match(/@template \{(.*?)\} ([a-zA-Z0-9_$]+)/);
|
|
1465
|
+
if (match) {
|
|
1466
|
+
pendingTemplates.push(match[2]);
|
|
1467
|
+
return true;
|
|
1468
|
+
}
|
|
1469
|
+
match = line.match(/@template (?:\{.*?\} )?\[([a-zA-Z0-9_$]+)=/);
|
|
1470
|
+
if (match) {
|
|
1471
|
+
pendingTemplates.push(match[1]);
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
match = line.match(/@template ([a-zA-Z0-9_$]+)(?![a-zA-Z0-9_$])/);
|
|
1475
|
+
if (match) {
|
|
1476
|
+
pendingTemplates.push(match[1]);
|
|
1477
|
+
return true;
|
|
1478
|
+
}
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
for (let line of lines) {
|
|
1482
|
+
line = line.trim();
|
|
1483
|
+
if (line[0] === '*') {
|
|
1484
|
+
line = line.slice(1).trim();
|
|
1485
|
+
}
|
|
1486
|
+
if (line.startsWith('@template')) {
|
|
1487
|
+
harvestTemplate(line);
|
|
1488
|
+
} else if (line.startsWith('@typedef')) {
|
|
1489
|
+
const {
|
|
1490
|
+
content: def,
|
|
1491
|
+
nextIndex
|
|
1492
|
+
} = extractCurlyContent(line);
|
|
1493
|
+
let name = line.substring(nextIndex).trim();
|
|
1494
|
+
// Drop description
|
|
1495
|
+
name = name.split(' ')[0];
|
|
1496
|
+
lastTypedef = expandType(def);
|
|
1497
|
+
// Ignore @typedef's that only refer to themselves in another file (see typedef-overwrite test)
|
|
1498
|
+
if (lastTypedef !== name) {
|
|
1499
|
+
typedefs[name] = lastTypedef;
|
|
1500
|
+
if (pendingTemplates.length) {
|
|
1501
|
+
typedefTemplates[name] = [...pendingTemplates];
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
pendingTemplates = [];
|
|
1505
|
+
} else if (line.startsWith('@property')) {
|
|
1506
|
+
var _lastTypedef;
|
|
1507
|
+
// class @property
|
|
1508
|
+
if (!lastTypedef) {
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const {
|
|
1512
|
+
content,
|
|
1513
|
+
nextIndex
|
|
1514
|
+
} = extractCurlyContent(line);
|
|
1515
|
+
const rest = line.substring(nextIndex);
|
|
1516
|
+
const propType = expandType(content);
|
|
1517
|
+
const [name, optional] = extractNameAndOptionality(rest);
|
|
1518
|
+
// console.log({name, optional, propType});
|
|
1519
|
+
const finalType = annotateOptional(propType, optional);
|
|
1520
|
+
if (((_lastTypedef = lastTypedef) == null ? void 0 : _lastTypedef.type) === 'object') {
|
|
1521
|
+
lastTypedef.properties[name] = finalType;
|
|
1522
|
+
} else {
|
|
1523
|
+
warn("not an extensible type", lastTypedef);
|
|
1524
|
+
}
|
|
1525
|
+
} else if (line.startsWith('@callback')) {
|
|
1526
|
+
const name = line.substring(9).trim();
|
|
1527
|
+
typedefs[name] = 'Function';
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* @typedef {ReturnType<typeof parseJSDoc>} ParseJSDocReturnType
|
|
1534
|
+
*/
|
|
1535
|
+
/**
|
|
1536
|
+
* @typedef {typeof expandTypeDepFree} ExpandType
|
|
1537
|
+
* @typedef {ReturnType<ExpandType>} ExpandTypeReturnType
|
|
1538
|
+
*/
|
|
1539
|
+
/**
|
|
1540
|
+
* Parses JSDoc comments to extract parameter type information.
|
|
1541
|
+
*
|
|
1542
|
+
* @param {string} src - The JSDoc comment string to parse.
|
|
1543
|
+
* @param {ExpandType} [expandType] - An optional function to process the types found in the JSDoc.
|
|
1544
|
+
* @returns {Record<string, ExpandTypeReturnType> | undefined} An object mapping parameter names to their parsed types, or undefined if no parameters are found.
|
|
1545
|
+
*/
|
|
1546
|
+
function parseJSDoc(src, expandType = expandTypeDepFree) {
|
|
1547
|
+
// Parse something like: @param {Object} [kwargs={}] Optional arguments.
|
|
1548
|
+
const regex = /@param \{(.*?)\} ([\[\]a-zA-Z0-9_$=\-\{\}\.'" ]+)/g;
|
|
1549
|
+
const matches = [...src.matchAll(regex)];
|
|
1550
|
+
/** @type {Record<string, ExpandTypeReturnType>} */
|
|
1551
|
+
const params = Object.create(null);
|
|
1552
|
+
matches.forEach(_ => {
|
|
1553
|
+
const type = expandType(_[1].trim());
|
|
1554
|
+
let name = _[2].trim();
|
|
1555
|
+
let optional = false;
|
|
1556
|
+
// Examples:
|
|
1557
|
+
// name: [kwargs={}] The configuration parameters.
|
|
1558
|
+
// name: [d = 1.0] Sample spacing
|
|
1559
|
+
if (name[0] === '[') {
|
|
1560
|
+
// Counting opening/closing brackets for perfect match
|
|
1561
|
+
let openCloseCount = 1;
|
|
1562
|
+
let i = 1;
|
|
1563
|
+
for (; i < name.length; i++) {
|
|
1564
|
+
const c = name[i];
|
|
1565
|
+
if (c === '[') {
|
|
1566
|
+
openCloseCount++;
|
|
1567
|
+
} else if (c === ']') {
|
|
1568
|
+
openCloseCount--;
|
|
1569
|
+
}
|
|
1570
|
+
if (openCloseCount === 0) {
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
// Afterwards name will be: d = 1.0
|
|
1575
|
+
name = name.substring(1, i);
|
|
1576
|
+
// mark it for the type:
|
|
1577
|
+
optional = true;
|
|
1578
|
+
}
|
|
1579
|
+
// Strip the rest (either leftover of optional value or description)
|
|
1580
|
+
name = name.split(' ')[0].split('=')[0].trim();
|
|
1581
|
+
const annotatedType = annotateOptional(type, optional);
|
|
1582
|
+
// Turn "options.stats[].unitsName" into ['options', 'stats', 'unitsName'].
|
|
1583
|
+
const parts = name.split(/[\[\]]*\./);
|
|
1584
|
+
let properties = params;
|
|
1585
|
+
for (const part of parts) {
|
|
1586
|
+
const toptype = properties[part];
|
|
1587
|
+
if (!toptype) {
|
|
1588
|
+
// No toptype means we resolved as far as possible, now we can add `annotatedType`.
|
|
1589
|
+
console.assert(part === parts.at(-1), 'Current part and last part should be the same.');
|
|
1590
|
+
properties[part] = annotatedType;
|
|
1591
|
+
} else if (toptype.type === "union") {
|
|
1592
|
+
const typeObject = toptype.members.find(_ => (_ == null ? void 0 : _.type) === 'object');
|
|
1593
|
+
properties = typeObject.properties;
|
|
1594
|
+
} else if (toptype.type === "array") {
|
|
1595
|
+
properties = toptype.elementType.properties;
|
|
1596
|
+
} else if (toptype.type === "object") {
|
|
1597
|
+
toptype.properties = toptype.properties || Object.create(null);
|
|
1598
|
+
properties = toptype.properties;
|
|
1599
|
+
} else {
|
|
1600
|
+
console.warn("parseJSDoc> Skipping @param, unseen syntax detected. Please check if your JSDoc is valid or open an issue about this!", {
|
|
1601
|
+
src,
|
|
1602
|
+
toptype,
|
|
1603
|
+
parts,
|
|
1604
|
+
annotatedType
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
if (Object.keys(params).length === 0) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
return params;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Infers a parameter type from its default value AST node.
|
|
1617
|
+
* Returns widened types like TypeScript does (`= 0` means `number`, not
|
|
1618
|
+
* literal `0`; `= null` widens to `any`).
|
|
1619
|
+
* Returns `undefined` when nothing useful can be inferred — the caller then
|
|
1620
|
+
* emits no check, exactly like an undocumented parameter today.
|
|
1621
|
+
* Shapes match what `expandType` produces so they can be embedded as-is.
|
|
1622
|
+
* @param {import('@babel/types').Node} node - The default value AST node.
|
|
1623
|
+
* @returns {string | object | undefined} Inferred type or `undefined` to skip.
|
|
1624
|
+
*/
|
|
1625
|
+
function inferTypeFromDefault$1(node) {
|
|
1626
|
+
if (!node) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
switch (node.type) {
|
|
1630
|
+
case 'NumericLiteral':
|
|
1631
|
+
return 'number';
|
|
1632
|
+
case 'StringLiteral':
|
|
1633
|
+
return 'string';
|
|
1634
|
+
case 'BooleanLiteral':
|
|
1635
|
+
return 'boolean';
|
|
1636
|
+
case 'BigIntLiteral':
|
|
1637
|
+
return {
|
|
1638
|
+
type: 'bigint'
|
|
1639
|
+
};
|
|
1640
|
+
case 'RegExpLiteral':
|
|
1641
|
+
return 'RegExp';
|
|
1642
|
+
case 'TemplateLiteral':
|
|
1643
|
+
return 'string';
|
|
1644
|
+
case 'ArrayExpression':
|
|
1645
|
+
return {
|
|
1646
|
+
type: 'array',
|
|
1647
|
+
elementType: 'any'
|
|
1648
|
+
};
|
|
1649
|
+
case 'ObjectExpression':
|
|
1650
|
+
return {
|
|
1651
|
+
type: 'object',
|
|
1652
|
+
properties: {}
|
|
1653
|
+
};
|
|
1654
|
+
case 'ArrowFunctionExpression':
|
|
1655
|
+
case 'FunctionExpression':
|
|
1656
|
+
return 'Function';
|
|
1657
|
+
case 'NewExpression':
|
|
1658
|
+
{
|
|
1659
|
+
const {
|
|
1660
|
+
callee
|
|
1661
|
+
} = node;
|
|
1662
|
+
if (callee.type === 'Identifier') {
|
|
1663
|
+
return callee.name;
|
|
1664
|
+
}
|
|
1665
|
+
break;
|
|
1666
|
+
}
|
|
1667
|
+
case 'UnaryExpression':
|
|
1668
|
+
{
|
|
1669
|
+
const {
|
|
1670
|
+
operator,
|
|
1671
|
+
argument
|
|
1672
|
+
} = node;
|
|
1673
|
+
if (operator === '!') {
|
|
1674
|
+
return 'boolean';
|
|
1675
|
+
}
|
|
1676
|
+
if (operator === 'void' || operator === 'typeof') {
|
|
1677
|
+
return operator === 'void' ? 'undefined' : 'string';
|
|
1678
|
+
}
|
|
1679
|
+
if ((operator === '-' || operator === '+') && argument.type === 'NumericLiteral') {
|
|
1680
|
+
return 'number';
|
|
1681
|
+
}
|
|
1682
|
+
if ((operator === '-' || operator === '+') && argument.type === 'BigIntLiteral') {
|
|
1683
|
+
return {
|
|
1684
|
+
type: 'bigint'
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Declaration-site ranks: the field always beats constructor assignments,
|
|
1693
|
+
// like TSC where the declared type wins over assigned values.
|
|
1694
|
+
const FIELD_JSDOC = 3;
|
|
1695
|
+
const CTOR_JSDOC = 2;
|
|
1696
|
+
const FIELD_INFER = 1;
|
|
1697
|
+
const CTOR_INFER = 0;
|
|
1698
|
+
/**
|
|
1699
|
+
* Reads the last block comment attached to a node.
|
|
1700
|
+
* @param {*} node - Babel AST node.
|
|
1701
|
+
* @returns {string|undefined} Comment text or undefined.
|
|
1702
|
+
*/
|
|
1703
|
+
function lastBlockComment(node) {
|
|
1704
|
+
const list = node == null ? void 0 : node.leadingComments;
|
|
1705
|
+
if (!list) {
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
1709
|
+
if (list[i].type === 'CommentBlock') {
|
|
1710
|
+
return list[i].value;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Extracts a `{...}`-typed JSDoc tag (`@type`, `@returns`) from a comment.
|
|
1716
|
+
* @param {string|undefined} comment - Comment text.
|
|
1717
|
+
* @param {string} tag - Tag name including `@`.
|
|
1718
|
+
* @param {Function} expandType - String type to structured type.
|
|
1719
|
+
* @returns {{type: *, raw: string}|undefined} Expanded type plus raw text.
|
|
1720
|
+
*/
|
|
1721
|
+
function tagType(comment, tag, expandType) {
|
|
1722
|
+
if (!comment) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const idx = comment.search(new RegExp(`${tag}(?=[\\s{])`));
|
|
1726
|
+
if (idx === -1) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const slice = comment.slice(idx);
|
|
1730
|
+
if (!slice.includes('{')) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
try {
|
|
1734
|
+
const {
|
|
1735
|
+
content
|
|
1736
|
+
} = extractCurlyContent(slice);
|
|
1737
|
+
if (!content || !content.trim()) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
return {
|
|
1741
|
+
type: expandType(content.trim()),
|
|
1742
|
+
raw: content.trim()
|
|
1743
|
+
};
|
|
1744
|
+
} catch (_unused) {
|
|
1745
|
+
// Unparseable annotation: fail open, harvest continues.
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Reads a static property name: identifiers and string literals, including
|
|
1750
|
+
* computed `['name']` forms. Anything dynamic yields undefined.
|
|
1751
|
+
* @param {*} key - Babel key node.
|
|
1752
|
+
* @param {boolean} computed - Whether the key position is computed.
|
|
1753
|
+
* @returns {string|undefined} Property name or undefined.
|
|
1754
|
+
*/
|
|
1755
|
+
function keyName(key, computed) {
|
|
1756
|
+
if (!key) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (key.type === 'Identifier' && !computed) {
|
|
1760
|
+
return key.name;
|
|
1761
|
+
}
|
|
1762
|
+
if (key.type === 'StringLiteral') {
|
|
1763
|
+
return key.value;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Unwraps parenthesized and TS `as`/`satisfies` nodes.
|
|
1768
|
+
* @param {*} node - Babel AST node.
|
|
1769
|
+
* @returns {*} Unwrapped node.
|
|
1770
|
+
*/
|
|
1771
|
+
function unwrap(node) {
|
|
1772
|
+
while (node && (node.type === 'ParenthesizedExpression' || node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression')) {
|
|
1773
|
+
node = node.expression;
|
|
1774
|
+
}
|
|
1775
|
+
return node;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Merges a harvested entry: higher rank wins the type, flags accumulate
|
|
1779
|
+
* (`optional` reflects runtime reality, `@readonly` anywhere counts).
|
|
1780
|
+
* Two disagreeing humans (JSDoc vs JSDoc) warn, like a TSC error.
|
|
1781
|
+
* @param {Record<string, object>} fields - Collected entries by name.
|
|
1782
|
+
* @param {string} name - Property name.
|
|
1783
|
+
* @param {object} entry - Entry with type, rank, raw, readonly, optional.
|
|
1784
|
+
* @param {Function} warn - Transpile-time warning function.
|
|
1785
|
+
* @param {string} className - Class name for messages.
|
|
1786
|
+
*/
|
|
1787
|
+
function record(fields, name, entry, warn, className) {
|
|
1788
|
+
const existing = fields[name];
|
|
1789
|
+
if (!existing) {
|
|
1790
|
+
fields[name] = entry;
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (entry.jsdoc && existing.jsdoc && entry.raw !== existing.raw) {
|
|
1794
|
+
warn(`harvestClassShape: ${className}.${name} has conflicting JSDoc types (${existing.raw} vs ${entry.raw})`);
|
|
1795
|
+
}
|
|
1796
|
+
const winner = entry.rank >= existing.rank ? entry : existing;
|
|
1797
|
+
winner.readonly = existing.readonly || entry.readonly;
|
|
1798
|
+
winner.optional = existing.optional || entry.optional;
|
|
1799
|
+
fields[name] = winner;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Turns a collected entry into a type struct, applying flags.
|
|
1803
|
+
* @param {object} entry - Collected entry.
|
|
1804
|
+
* @returns {*} Type struct or name.
|
|
1805
|
+
*/
|
|
1806
|
+
function entryToType(entry) {
|
|
1807
|
+
var _entry$type;
|
|
1808
|
+
const t = (_entry$type = entry.type) != null ? _entry$type : 'any';
|
|
1809
|
+
if (!entry.readonly && !entry.optional) {
|
|
1810
|
+
return t;
|
|
1811
|
+
}
|
|
1812
|
+
if (t && typeof t === 'object') {
|
|
1813
|
+
return _extends({}, t, entry.readonly ? {
|
|
1814
|
+
readonly: true
|
|
1815
|
+
} : {}, entry.optional ? {
|
|
1816
|
+
optional: true
|
|
1817
|
+
} : {});
|
|
1818
|
+
}
|
|
1819
|
+
const out = {
|
|
1820
|
+
type: t
|
|
1821
|
+
};
|
|
1822
|
+
if (entry.readonly) {
|
|
1823
|
+
out.readonly = true;
|
|
1824
|
+
}
|
|
1825
|
+
if (entry.optional) {
|
|
1826
|
+
out.optional = true;
|
|
1827
|
+
}
|
|
1828
|
+
return out;
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Records `this.x = ...` (or `+=`, `++`) assignments.
|
|
1832
|
+
* @param {*} left - Assignment target.
|
|
1833
|
+
* @param {*} right - Assigned value or undefined for op-assign/update.
|
|
1834
|
+
* @param {string|undefined} comment - Leading comment text.
|
|
1835
|
+
* @param {boolean} conditional - True inside conditionals (counts as optional).
|
|
1836
|
+
* @param {object} ctx - Harvest context with fields, expandType, warn, className.
|
|
1837
|
+
*/
|
|
1838
|
+
function recordThisAssign(left, right, comment, conditional, ctx) {
|
|
1839
|
+
var _declared$type;
|
|
1840
|
+
const {
|
|
1841
|
+
fields,
|
|
1842
|
+
expandType,
|
|
1843
|
+
warn,
|
|
1844
|
+
className
|
|
1845
|
+
} = ctx;
|
|
1846
|
+
left = unwrap(left);
|
|
1847
|
+
if (!left || left.type !== 'MemberExpression' || left.computed) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (!left.object || left.object.type !== 'ThisExpression') {
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
const name = keyName(left.property, false);
|
|
1854
|
+
if (name === undefined) {
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
const declared = tagType(comment, '@type', expandType);
|
|
1858
|
+
if (right === undefined) {
|
|
1859
|
+
// Op-assign/update (`+=`, `++`): exists, type unknown.
|
|
1860
|
+
record(fields, name, {
|
|
1861
|
+
type: 'any',
|
|
1862
|
+
rank: CTOR_INFER,
|
|
1863
|
+
jsdoc: false,
|
|
1864
|
+
optional: conditional
|
|
1865
|
+
}, warn, className);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const inferred = inferTypeFromDefault$1(unwrap(right));
|
|
1869
|
+
const type = (_declared$type = declared == null ? void 0 : declared.type) != null ? _declared$type : inferred;
|
|
1870
|
+
if (type === undefined) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
record(fields, name, {
|
|
1874
|
+
type,
|
|
1875
|
+
rank: declared ? CTOR_JSDOC : CTOR_INFER,
|
|
1876
|
+
jsdoc: declared !== undefined,
|
|
1877
|
+
raw: declared == null ? void 0 : declared.raw,
|
|
1878
|
+
optional: conditional
|
|
1879
|
+
}, warn, className);
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Handles `Object.assign(this, {...})` with inline object literals.
|
|
1883
|
+
* @param {*} node - CallExpression node.
|
|
1884
|
+
* @param {boolean} conditional - True inside conditionals.
|
|
1885
|
+
* @param {object} ctx - Harvest context.
|
|
1886
|
+
* @returns {boolean} True when the call was `Object.assign` on `this`.
|
|
1887
|
+
*/
|
|
1888
|
+
function recordObjectAssign(node, conditional, ctx) {
|
|
1889
|
+
var _callee$object, _callee$property;
|
|
1890
|
+
const {
|
|
1891
|
+
fields,
|
|
1892
|
+
expandType,
|
|
1893
|
+
warn,
|
|
1894
|
+
className
|
|
1895
|
+
} = ctx;
|
|
1896
|
+
const {
|
|
1897
|
+
callee,
|
|
1898
|
+
arguments: args
|
|
1899
|
+
} = node;
|
|
1900
|
+
if (!callee || callee.type !== 'MemberExpression' || callee.computed) {
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
if (((_callee$object = callee.object) == null ? void 0 : _callee$object.type) !== 'Identifier' || callee.object.name !== 'Object') {
|
|
1904
|
+
return false;
|
|
1905
|
+
}
|
|
1906
|
+
if (((_callee$property = callee.property) == null ? void 0 : _callee$property.type) !== 'Identifier' || callee.property.name !== 'assign') {
|
|
1907
|
+
return false;
|
|
1908
|
+
}
|
|
1909
|
+
if (!args.length || args[0].type !== 'ThisExpression') {
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
for (const arg of args.slice(1)) {
|
|
1913
|
+
if (!arg || arg.type !== 'ObjectExpression') {
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
for (const prop of arg.properties) {
|
|
1917
|
+
var _declared$type2;
|
|
1918
|
+
if (!prop || prop.type !== 'ObjectProperty') {
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
const name = keyName(prop.key, prop.computed);
|
|
1922
|
+
if (name === undefined) {
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
const comment = lastBlockComment(prop);
|
|
1926
|
+
const declared = tagType(comment, '@type', expandType);
|
|
1927
|
+
const type = (_declared$type2 = declared == null ? void 0 : declared.type) != null ? _declared$type2 : inferTypeFromDefault$1(unwrap(prop.value));
|
|
1928
|
+
if (type === undefined) {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
record(fields, name, {
|
|
1932
|
+
type,
|
|
1933
|
+
rank: declared ? CTOR_JSDOC : CTOR_INFER,
|
|
1934
|
+
jsdoc: declared !== undefined,
|
|
1935
|
+
raw: declared == null ? void 0 : declared.raw,
|
|
1936
|
+
optional: conditional
|
|
1937
|
+
}, warn, className);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return true;
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Walks an expression for `this.x` writes. Stops at function boundaries:
|
|
1944
|
+
* deferred writes (callbacks) are out of scope.
|
|
1945
|
+
* @param {*} expr - Babel expression node.
|
|
1946
|
+
* @param {boolean} conditional - True inside conditionals.
|
|
1947
|
+
* @param {object} ctx - Harvest context.
|
|
1948
|
+
* @param {*} stmt - Enclosing statement carrying leading comments.
|
|
1949
|
+
*/
|
|
1950
|
+
function walkExpression(expr, conditional, ctx, stmt) {
|
|
1951
|
+
if (!expr) {
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
switch (expr.type) {
|
|
1955
|
+
case 'AssignmentExpression':
|
|
1956
|
+
{
|
|
1957
|
+
const comment = lastBlockComment(stmt);
|
|
1958
|
+
if (expr.operator === '=') {
|
|
1959
|
+
recordThisAssign(expr.left, expr.right, comment, conditional, ctx);
|
|
1960
|
+
walkExpression(expr.right, conditional, ctx, stmt);
|
|
1961
|
+
} else {
|
|
1962
|
+
recordThisAssign(expr.left, undefined, comment, conditional, ctx);
|
|
1963
|
+
}
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
case 'UpdateExpression':
|
|
1967
|
+
recordThisAssign(expr.argument, undefined, lastBlockComment(stmt), conditional, ctx);
|
|
1968
|
+
break;
|
|
1969
|
+
case 'SequenceExpression':
|
|
1970
|
+
for (const each of expr.expressions) {
|
|
1971
|
+
walkExpression(each, conditional, ctx, stmt);
|
|
1972
|
+
}
|
|
1973
|
+
break;
|
|
1974
|
+
case 'LogicalExpression':
|
|
1975
|
+
walkExpression(expr.right, true, ctx, stmt);
|
|
1976
|
+
break;
|
|
1977
|
+
case 'ConditionalExpression':
|
|
1978
|
+
walkExpression(expr.consequent, true, ctx, stmt);
|
|
1979
|
+
walkExpression(expr.alternate, true, ctx, stmt);
|
|
1980
|
+
break;
|
|
1981
|
+
case 'CallExpression':
|
|
1982
|
+
recordObjectAssign(expr, conditional, ctx);
|
|
1983
|
+
break;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Walks a statement for `this.x` writes. Conditional wrappers mark entries
|
|
1988
|
+
* optional; nested functions are boundaries.
|
|
1989
|
+
* @param {*} st - Babel statement node.
|
|
1990
|
+
* @param {boolean} conditional - True inside conditionals.
|
|
1991
|
+
* @param {object} ctx - Harvest context.
|
|
1992
|
+
*/
|
|
1993
|
+
function walkStatement(st, conditional, ctx) {
|
|
1994
|
+
if (!st) {
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
switch (st.type) {
|
|
1998
|
+
case 'BlockStatement':
|
|
1999
|
+
for (const each of st.body) {
|
|
2000
|
+
walkStatement(each, conditional, ctx);
|
|
2001
|
+
}
|
|
2002
|
+
break;
|
|
2003
|
+
case 'ExpressionStatement':
|
|
2004
|
+
walkExpression(st.expression, conditional, ctx, st);
|
|
2005
|
+
break;
|
|
2006
|
+
case 'ReturnStatement':
|
|
2007
|
+
walkExpression(st.argument, conditional, ctx, st);
|
|
2008
|
+
break;
|
|
2009
|
+
case 'IfStatement':
|
|
2010
|
+
walkStatement(st.consequent, true, ctx);
|
|
2011
|
+
walkStatement(st.alternate, true, ctx);
|
|
2012
|
+
break;
|
|
2013
|
+
case 'WhileStatement':
|
|
2014
|
+
case 'ForStatement':
|
|
2015
|
+
case 'ForInStatement':
|
|
2016
|
+
case 'ForOfStatement':
|
|
2017
|
+
walkStatement(st.body, true, ctx);
|
|
2018
|
+
break;
|
|
2019
|
+
case 'DoWhileStatement':
|
|
2020
|
+
walkStatement(st.body, conditional, ctx);
|
|
2021
|
+
break;
|
|
2022
|
+
case 'SwitchStatement':
|
|
2023
|
+
for (const each of st.cases) {
|
|
2024
|
+
for (const consequent of each.consequent) {
|
|
2025
|
+
walkStatement(consequent, true, ctx);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
break;
|
|
2029
|
+
case 'TryStatement':
|
|
2030
|
+
walkStatement(st.block, conditional, ctx);
|
|
2031
|
+
if (st.handler) {
|
|
2032
|
+
walkStatement(st.handler.body, true, ctx);
|
|
2033
|
+
}
|
|
2034
|
+
if (st.finalizer) {
|
|
2035
|
+
walkStatement(st.finalizer, conditional, ctx);
|
|
2036
|
+
}
|
|
2037
|
+
break;
|
|
2038
|
+
case 'LabeledStatement':
|
|
2039
|
+
walkStatement(st.body, conditional, ctx);
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Reads the single parameter name of a setter.
|
|
2045
|
+
* @param {*} param - Babel parameter node.
|
|
2046
|
+
* @returns {string|undefined} Name or undefined.
|
|
2047
|
+
*/
|
|
2048
|
+
function setterParamName(param) {
|
|
2049
|
+
if (!param) {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (param.type === 'Identifier') {
|
|
2053
|
+
return param.name;
|
|
2054
|
+
}
|
|
2055
|
+
if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') {
|
|
2056
|
+
return param.left.name;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Reads a setter's value type: its `@param` JSDoc first, `@type` fallback.
|
|
2061
|
+
* @param {*} set - Babel ClassMethod (kind set) node.
|
|
2062
|
+
* @param {Function} expandType - String type to structured type.
|
|
2063
|
+
* @returns {{type: *, raw: string|undefined, jsdoc: boolean}} Type plus metadata.
|
|
2064
|
+
*/
|
|
2065
|
+
function setterType(set, expandType) {
|
|
2066
|
+
const setComment = lastBlockComment(set);
|
|
2067
|
+
const paramName = setterParamName(set.params[0]);
|
|
2068
|
+
const params = parseJSDoc(setComment != null ? setComment : '', expandType);
|
|
2069
|
+
if (paramName && params && params[paramName] !== undefined) {
|
|
2070
|
+
return {
|
|
2071
|
+
type: params[paramName],
|
|
2072
|
+
raw: undefined,
|
|
2073
|
+
jsdoc: true
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
const tagged = tagType(setComment, '@type', expandType);
|
|
2077
|
+
if (tagged) {
|
|
2078
|
+
return {
|
|
2079
|
+
type: tagged.type,
|
|
2080
|
+
raw: tagged.raw,
|
|
2081
|
+
jsdoc: true
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
1073
2084
|
return {
|
|
1074
|
-
|
|
1075
|
-
|
|
2085
|
+
type: 'any',
|
|
2086
|
+
raw: undefined,
|
|
2087
|
+
jsdoc: false
|
|
1076
2088
|
};
|
|
1077
2089
|
}
|
|
1078
2090
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
* @param {
|
|
1085
|
-
* @param {
|
|
1086
|
-
* @param {
|
|
1087
|
-
* @
|
|
2091
|
+
* Harvests the instance shape of a class: field declarations (JSDoc wins,
|
|
2092
|
+
* else inferred), constructor `this.x` writes (field site wins conflicts),
|
|
2093
|
+
* methods as `Function`, getter/setter pairs as writable and getter-only as
|
|
2094
|
+
* readonly. Statics, privates and dynamic keys are skipped.
|
|
2095
|
+
* @param {*} node - Babel ClassDeclaration node.
|
|
2096
|
+
* @param {object} opts - Options with expandType and warn.
|
|
2097
|
+
* @param {Function} opts.expandType - String type to structured type.
|
|
2098
|
+
* @param {Function} opts.warn - Transpile-time warning function.
|
|
2099
|
+
* @returns {{name: string, shape: object}|undefined} Class name plus object shape.
|
|
1088
2100
|
*/
|
|
1089
|
-
function
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (type !== '
|
|
2101
|
+
function harvestClassShape(node, {
|
|
2102
|
+
expandType,
|
|
2103
|
+
warn
|
|
2104
|
+
}) {
|
|
2105
|
+
const id = node == null ? void 0 : node.id;
|
|
2106
|
+
if (!id || id.type !== 'Identifier' || !id.name) {
|
|
1095
2107
|
return;
|
|
1096
2108
|
}
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
2109
|
+
const className = id.name;
|
|
2110
|
+
const ctx = {
|
|
2111
|
+
fields: {},
|
|
2112
|
+
gets: {},
|
|
2113
|
+
sets: {},
|
|
2114
|
+
expandType,
|
|
2115
|
+
warn,
|
|
2116
|
+
className
|
|
2117
|
+
};
|
|
2118
|
+
let ctor = null;
|
|
2119
|
+
for (const el of node.body.body) {
|
|
2120
|
+
if (el.type === 'ClassPrivateProperty' || el.type === 'ClassPrivateMethod') {
|
|
2121
|
+
continue;
|
|
1103
2122
|
}
|
|
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;
|
|
2123
|
+
const name = keyName(el.key, el.computed);
|
|
2124
|
+
if (name === undefined) {
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
if (el.type === 'ClassProperty' || el.type === 'PropertyDefinition') {
|
|
2128
|
+
var _declared$type3;
|
|
2129
|
+
if (el.static || el.declare) {
|
|
2130
|
+
continue;
|
|
1116
2131
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2132
|
+
const comment = lastBlockComment(el);
|
|
2133
|
+
const declared = tagType(comment, '@type', expandType);
|
|
2134
|
+
const inferred = el.value ? inferTypeFromDefault$1(unwrap(el.value)) : undefined;
|
|
2135
|
+
const type = (_declared$type3 = declared == null ? void 0 : declared.type) != null ? _declared$type3 : inferred;
|
|
2136
|
+
if (type === undefined) {
|
|
1121
2137
|
continue;
|
|
1122
2138
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
} else {
|
|
1135
|
-
warn("not an extensible type", lastTypedef);
|
|
2139
|
+
record(ctx.fields, name, {
|
|
2140
|
+
type,
|
|
2141
|
+
rank: declared ? FIELD_JSDOC : FIELD_INFER,
|
|
2142
|
+
jsdoc: declared !== undefined,
|
|
2143
|
+
raw: declared == null ? void 0 : declared.raw,
|
|
2144
|
+
readonly: el.readonly === true || (comment ? /@readonly(?![\w])/.test(comment) : false),
|
|
2145
|
+
optional: el.optional === true
|
|
2146
|
+
}, warn, className);
|
|
2147
|
+
} else if (el.type === 'ClassMethod' || el.type === 'ClassPrivateMethod') {
|
|
2148
|
+
if (el.static) {
|
|
2149
|
+
continue;
|
|
1136
2150
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
2151
|
+
if (el.kind === 'constructor') {
|
|
2152
|
+
ctor = el;
|
|
2153
|
+
} else if (el.kind === 'method') {
|
|
2154
|
+
var _ctx$fields$name;
|
|
2155
|
+
ctx.fields[name] = (_ctx$fields$name = ctx.fields[name]) != null ? _ctx$fields$name : {
|
|
2156
|
+
type: 'Function',
|
|
2157
|
+
rank: FIELD_INFER,
|
|
2158
|
+
jsdoc: false
|
|
2159
|
+
};
|
|
2160
|
+
} else if (el.kind === 'get') {
|
|
2161
|
+
ctx.gets[name] = el;
|
|
2162
|
+
} else if (el.kind === 'set') {
|
|
2163
|
+
ctx.sets[name] = el;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
for (const name of Object.keys(ctx.gets)) {
|
|
2168
|
+
var _tagType, _ref, _getType$type, _getType$raw;
|
|
2169
|
+
const get = ctx.gets[name];
|
|
2170
|
+
const set = ctx.sets[name];
|
|
2171
|
+
const getComment = lastBlockComment(get);
|
|
2172
|
+
const getType = (_tagType = tagType(getComment, '@type', expandType)) != null ? _tagType : tagType(getComment, '@returns', expandType);
|
|
2173
|
+
const setInfo = set ? setterType(set, expandType) : undefined;
|
|
2174
|
+
const type = (_ref = (_getType$type = getType == null ? void 0 : getType.type) != null ? _getType$type : setInfo == null ? void 0 : setInfo.type) != null ? _ref : 'any';
|
|
2175
|
+
record(ctx.fields, name, {
|
|
2176
|
+
type,
|
|
2177
|
+
rank: getType || setInfo != null && setInfo.jsdoc ? FIELD_JSDOC : FIELD_INFER,
|
|
2178
|
+
jsdoc: Boolean(getType || (setInfo == null ? void 0 : setInfo.jsdoc)),
|
|
2179
|
+
raw: (_getType$raw = getType == null ? void 0 : getType.raw) != null ? _getType$raw : setInfo == null ? void 0 : setInfo.raw,
|
|
2180
|
+
readonly: !set
|
|
2181
|
+
}, warn, className);
|
|
2182
|
+
}
|
|
2183
|
+
for (const name of Object.keys(ctx.sets)) {
|
|
2184
|
+
if (ctx.gets[name]) {
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
const setInfo = setterType(ctx.sets[name], expandType);
|
|
2188
|
+
record(ctx.fields, name, {
|
|
2189
|
+
type: setInfo.type,
|
|
2190
|
+
rank: setInfo.jsdoc ? FIELD_JSDOC : FIELD_INFER,
|
|
2191
|
+
jsdoc: setInfo.jsdoc,
|
|
2192
|
+
raw: setInfo.raw
|
|
2193
|
+
}, warn, className);
|
|
2194
|
+
}
|
|
2195
|
+
if (ctor && ctor.body) {
|
|
2196
|
+
for (const st of ctor.body.body) {
|
|
2197
|
+
walkStatement(st, false, ctx);
|
|
1140
2198
|
}
|
|
1141
2199
|
}
|
|
2200
|
+
const properties = {};
|
|
2201
|
+
for (const name of Object.keys(ctx.fields)) {
|
|
2202
|
+
properties[name] = entryToType(ctx.fields[name]);
|
|
2203
|
+
}
|
|
2204
|
+
if (!Object.keys(properties).length) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
name: className,
|
|
2209
|
+
shape: {
|
|
2210
|
+
type: 'object',
|
|
2211
|
+
properties
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
1142
2214
|
}
|
|
1143
2215
|
|
|
1144
2216
|
/**
|
|
@@ -1190,89 +2262,6 @@ function nodeIsFunctionLike(node) {
|
|
|
1190
2262
|
return false;
|
|
1191
2263
|
}
|
|
1192
2264
|
|
|
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
2265
|
/**
|
|
1277
2266
|
* @param {string} src - JSDoc comment of the setter.
|
|
1278
2267
|
* @param {Function} expandType - The expandType function.
|
|
@@ -1305,9 +2294,6 @@ function parseJSDocSetter(src, expandType = expandTypeDepFree) {
|
|
|
1305
2294
|
function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
|
|
1306
2295
|
const regexTemplateTyped = /@template \{(.*?)\} ([a-zA-Z0-9_$=]+)/g;
|
|
1307
2296
|
const matches = [...src.matchAll(regexTemplateTyped)];
|
|
1308
|
-
if (!matches.length) {
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
2297
|
/** @type {Record<string, ExpandTypeReturnType>} */
|
|
1312
2298
|
const templates = Object.create(null);
|
|
1313
2299
|
matches.forEach(_ => {
|
|
@@ -1315,22 +2301,23 @@ function parseJSDocTemplates(src, expandType = expandTypeDepFree) {
|
|
|
1315
2301
|
const name = _[2].trim();
|
|
1316
2302
|
templates[name] = type;
|
|
1317
2303
|
});
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
2304
|
+
// `@template [A=X]` defaults: constraint X under name A.
|
|
2305
|
+
const regexTemplateDefault = /@template \[([a-zA-Z0-9_$]+)=([^\]]+)\]/g;
|
|
2306
|
+
for (const match of src.matchAll(regexTemplateDefault)) {
|
|
2307
|
+
templates[match[1]] = expandType(match[2].trim());
|
|
2308
|
+
}
|
|
2309
|
+
// Bare `@template T`: unconstrained, stands in as any.
|
|
2310
|
+
const regexTemplateBare = /@template ([a-zA-Z0-9_$]+)(?![a-zA-Z0-9_$])/g;
|
|
2311
|
+
for (const match of src.matchAll(regexTemplateBare)) {
|
|
2312
|
+
const name = match[1];
|
|
2313
|
+
if (!(name in templates)) {
|
|
2314
|
+
templates[name] = 'any';
|
|
1330
2315
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2316
|
+
}
|
|
2317
|
+
if (!Object.keys(templates).length) {
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
return templates;
|
|
1334
2321
|
}
|
|
1335
2322
|
|
|
1336
2323
|
/**
|
|
@@ -3516,6 +4503,9 @@ class Stringifier {
|
|
|
3516
4503
|
* @typedef {object} Options
|
|
3517
4504
|
* @property {boolean} [forceCurly] - Determines whether curly braces are enforced in Stringifier.
|
|
3518
4505
|
* @property {boolean} [validateDivision] - Indicates whether division operations should be validated.
|
|
4506
|
+
* @property {boolean} [inspectIndexedAccess] - Indicates whether indexed accesses
|
|
4507
|
+
* like `arr[i]` should be wrapped for bounds and integer validation. Disable
|
|
4508
|
+
* to drop indexed access inspection entirely. Defaults to true.
|
|
3519
4509
|
* @property {import('./parseJSDoc.js').ExpandType} [expandType] - A function that expands shorthand types into full descriptions.
|
|
3520
4510
|
* @property {string} [filename] - The name of a file to which the instance pertains.
|
|
3521
4511
|
* @property {boolean} [addHeader] - Whether to add import declarations headers. Defaults to true.
|
|
@@ -3528,6 +4518,7 @@ class Asserter extends Stringifier {
|
|
|
3528
4518
|
constructor({
|
|
3529
4519
|
forceCurly = true,
|
|
3530
4520
|
validateDivision = true,
|
|
4521
|
+
inspectIndexedAccess = true,
|
|
3531
4522
|
expandType = expandTypeDepFree,
|
|
3532
4523
|
filename,
|
|
3533
4524
|
addHeader = true,
|
|
@@ -3583,10 +4574,13 @@ class Asserter extends Stringifier {
|
|
|
3583
4574
|
};
|
|
3584
4575
|
/** @type {Record<string, object>} */
|
|
3585
4576
|
this.typedefs = {};
|
|
4577
|
+
/** @type {Record<string, string[]>} */
|
|
4578
|
+
this.typedefTemplates = {};
|
|
3586
4579
|
/** @type {string[]} */
|
|
3587
4580
|
this.addLaterImportNamespaceSpecifier = [];
|
|
3588
4581
|
this.forceCurly = forceCurly;
|
|
3589
4582
|
this.validateDivision = validateDivision;
|
|
4583
|
+
this.inspectIndexedAccess = inspectIndexedAccess;
|
|
3590
4584
|
// @todo collect every type + manually validate as test set
|
|
3591
4585
|
// + implement expandType using Babel Flow type parser aswell
|
|
3592
4586
|
this.expandType = expandType;
|
|
@@ -3644,6 +4638,21 @@ class Asserter extends Stringifier {
|
|
|
3644
4638
|
const id_ = this.toSource(id);
|
|
3645
4639
|
let out = super.ClassDeclaration(node);
|
|
3646
4640
|
out += `${this.spaces}registerClass(${id_});`;
|
|
4641
|
+
const harvested = harvestClassShape(node, {
|
|
4642
|
+
expandType: this.expandType,
|
|
4643
|
+
warn: this.warn.bind(this)
|
|
4644
|
+
});
|
|
4645
|
+
if (harvested) {
|
|
4646
|
+
// Hand-written typedefs win: emitting a second registerTypedef for the
|
|
4647
|
+
// same name would be last-wins deterministic but noisy, so the harvest
|
|
4648
|
+
// step skips names already present in this.typedefs (populated in File).
|
|
4649
|
+
if (this.typedefs[harvested.name]) {
|
|
4650
|
+
this.warn(`harvestClassShape: skipping harvested shape for '${harvested.name}', hand-written typedef wins`);
|
|
4651
|
+
} else {
|
|
4652
|
+
const json = simplifyTypeToSource(harvested.shape);
|
|
4653
|
+
out += `\n${this.spaces}registerTypedef('${harvested.name}', ${json});`;
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
3647
4656
|
return out;
|
|
3648
4657
|
}
|
|
3649
4658
|
/**
|
|
@@ -4393,6 +5402,9 @@ class Asserter extends Stringifier {
|
|
|
4393
5402
|
object,
|
|
4394
5403
|
property
|
|
4395
5404
|
} = node;
|
|
5405
|
+
if (!this.inspectIndexedAccess) {
|
|
5406
|
+
return super.MemberExpression(node);
|
|
5407
|
+
}
|
|
4396
5408
|
if (!computed || object.type === 'Super') {
|
|
4397
5409
|
return super.MemberExpression(node);
|
|
4398
5410
|
}
|
|
@@ -4409,6 +5421,43 @@ class Asserter extends Stringifier {
|
|
|
4409
5421
|
if (parent.type === 'UnaryExpression' && parent.operator === 'delete' && parent.argument === node) {
|
|
4410
5422
|
return super.MemberExpression(node);
|
|
4411
5423
|
}
|
|
5424
|
+
// Destructuring, rest and loop targets are assigned to, never read.
|
|
5425
|
+
if (parent.type === 'ArrayPattern') {
|
|
5426
|
+
return super.MemberExpression(node);
|
|
5427
|
+
}
|
|
5428
|
+
if (parent.type === 'RestElement' && parent.argument === node) {
|
|
5429
|
+
return super.MemberExpression(node);
|
|
5430
|
+
}
|
|
5431
|
+
if (parent.type === 'ObjectProperty' && parent.value === node) {
|
|
5432
|
+
// `({p: a[i]})` reads but `({p: a[i]} = ...)` writes: bypass inside patterns only.
|
|
5433
|
+
const grandparent = this.parents[this.parents.findLastIndex(_ => _ === parent) - 1];
|
|
5434
|
+
if ((grandparent == null ? void 0 : grandparent.type) === 'ObjectPattern') {
|
|
5435
|
+
return super.MemberExpression(node);
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
if (parent.type === 'AssignmentPattern' && parent.left === node) {
|
|
5439
|
+
return super.MemberExpression(node);
|
|
5440
|
+
}
|
|
5441
|
+
if ((parent.type === 'ForOfStatement' || parent.type === 'ForInStatement') && parent.left === node) {
|
|
5442
|
+
return super.MemberExpression(node);
|
|
5443
|
+
}
|
|
5444
|
+
// Method calls and tags carry their base as `this`; wrapping the callee
|
|
5445
|
+
// would silently rebind it to undefined.
|
|
5446
|
+
if ((parent.type === 'CallExpression' || parent.type === 'OptionalCallExpression') && parent.callee === node) {
|
|
5447
|
+
return super.MemberExpression(node);
|
|
5448
|
+
}
|
|
5449
|
+
if (parent.type === 'TaggedTemplateExpression' && parent.tag === node) {
|
|
5450
|
+
return super.MemberExpression(node);
|
|
5451
|
+
}
|
|
5452
|
+
// `new a[i](...)` must stay `new (inspectIndexedAccess(...))(...)`:
|
|
5453
|
+
// without parens it parses as `(new inspectIndexedAccess(...))(...)`,
|
|
5454
|
+
// constructing the wrapper instead of the accessed value.
|
|
5455
|
+
if (parent.type === 'NewExpression' && parent.callee === node) {
|
|
5456
|
+
const _object_ = this.toSource(object);
|
|
5457
|
+
const _property_ = this.toSource(property);
|
|
5458
|
+
const _loc5 = this.getName(node);
|
|
5459
|
+
return `(inspectIndexedAccess(${_object_}, ${_property_}, ${JSON.stringify(_loc5)}))`;
|
|
5460
|
+
}
|
|
4412
5461
|
}
|
|
4413
5462
|
const object_ = this.toSource(object);
|
|
4414
5463
|
const property_ = this.toSource(property);
|
|
@@ -4451,7 +5500,7 @@ class Asserter extends Stringifier {
|
|
|
4451
5500
|
if (comments) {
|
|
4452
5501
|
for (const comment of comments) {
|
|
4453
5502
|
const warn = this.warn.bind(this);
|
|
4454
|
-
parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
|
|
5503
|
+
parseJSDocTypedef(this.typedefs, this.typedefTemplates, warn, comment, this.expandType);
|
|
4455
5504
|
}
|
|
4456
5505
|
}
|
|
4457
5506
|
//console.log("this.typedefs", this.typedefs);
|
|
@@ -4459,7 +5508,8 @@ class Asserter extends Stringifier {
|
|
|
4459
5508
|
for (const name in this.typedefs) {
|
|
4460
5509
|
const typedef = this.typedefs[name];
|
|
4461
5510
|
const json = simplifyTypeToSource(typedef);
|
|
4462
|
-
|
|
5511
|
+
const params = this.typedefTemplates[name];
|
|
5512
|
+
out += params != null && params.length ? `registerTypedef('${name}', ${json}, ${JSON.stringify(params)});\n` : `registerTypedef('${name}', ${json});\n`;
|
|
4463
5513
|
}
|
|
4464
5514
|
const code = this.toSource(program) + '\n';
|
|
4465
5515
|
out += code;
|
|
@@ -4729,7 +5779,13 @@ function toSourceBabelTS(node) {
|
|
|
4729
5779
|
{
|
|
4730
5780
|
const name = toSourceBabelTS(node.typeName);
|
|
4731
5781
|
if (!node.typeParameters) {
|
|
4732
|
-
//
|
|
5782
|
+
// Bare `Object` is the empty object type, matching expandType().
|
|
5783
|
+
if (name === 'Object') {
|
|
5784
|
+
return {
|
|
5785
|
+
type: 'object',
|
|
5786
|
+
properties: {}
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
4733
5789
|
// Bare reference: Identifier gives name, TSQualifiedName gives dotted path.
|
|
4734
5790
|
return name;
|
|
4735
5791
|
}
|
|
@@ -4790,6 +5846,8 @@ function toSourceBabelTS(node) {
|
|
|
4790
5846
|
}
|
|
4791
5847
|
case 'TSStringKeyword':
|
|
4792
5848
|
return 'string';
|
|
5849
|
+
case 'TSSymbolKeyword':
|
|
5850
|
+
return 'symbol';
|
|
4793
5851
|
case 'TSNumberKeyword':
|
|
4794
5852
|
return 'number';
|
|
4795
5853
|
case 'TSIntersectionType':
|
|
@@ -4871,6 +5929,8 @@ function toSourceBabelTS(node) {
|
|
|
4871
5929
|
case 'ParenthesizedType':
|
|
4872
5930
|
// fall-through for parentheses
|
|
4873
5931
|
return toSourceBabelTS(node.type);
|
|
5932
|
+
case 'TSParenthesizedType':
|
|
5933
|
+
return toSourceBabelTS(node.typeAnnotation);
|
|
4874
5934
|
case 'LastTypeNode':
|
|
4875
5935
|
return toSourceBabelTS(node.qualifier);
|
|
4876
5936
|
case 'TSTypeQuery':
|
|
@@ -4879,11 +5939,120 @@ function toSourceBabelTS(node) {
|
|
|
4879
5939
|
type: 'typeof',
|
|
4880
5940
|
argument
|
|
4881
5941
|
};
|
|
5942
|
+
case 'TSConditionalType':
|
|
5943
|
+
{
|
|
5944
|
+
const checkType = toSourceBabelTS(node.checkType);
|
|
5945
|
+
const extendsType = toSourceBabelTS(node.extendsType);
|
|
5946
|
+
const trueType = toSourceBabelTS(node.trueType);
|
|
5947
|
+
const falseType = toSourceBabelTS(node.falseType);
|
|
5948
|
+
return {
|
|
5949
|
+
type: 'condition',
|
|
5950
|
+
checkType,
|
|
5951
|
+
extendsType,
|
|
5952
|
+
trueType,
|
|
5953
|
+
falseType
|
|
5954
|
+
};
|
|
5955
|
+
}
|
|
5956
|
+
case 'TSIndexedAccessType':
|
|
5957
|
+
{
|
|
5958
|
+
const index = toSourceBabelTS(node.indexType);
|
|
5959
|
+
const object = toSourceBabelTS(node.objectType);
|
|
5960
|
+
return {
|
|
5961
|
+
type: 'indexedAccess',
|
|
5962
|
+
index,
|
|
5963
|
+
object
|
|
5964
|
+
};
|
|
5965
|
+
}
|
|
5966
|
+
case 'TSMappedType':
|
|
5967
|
+
{
|
|
5968
|
+
const param = node.typeParameter;
|
|
5969
|
+
const nameNode = param == null ? void 0 : param.name;
|
|
5970
|
+
const element = typeof nameNode === 'string' ? nameNode : toSourceBabelTS(nameNode);
|
|
5971
|
+
const iterable = toSourceBabelTS(param == null ? void 0 : param.constraint);
|
|
5972
|
+
const result = toSourceBabelTS(node.typeAnnotation);
|
|
5973
|
+
const out = {
|
|
5974
|
+
type: 'mapping',
|
|
5975
|
+
iterable,
|
|
5976
|
+
element,
|
|
5977
|
+
result
|
|
5978
|
+
};
|
|
5979
|
+
if (node.nameType) {
|
|
5980
|
+
out.nameType = toSourceBabelTS(node.nameType);
|
|
5981
|
+
}
|
|
5982
|
+
if (node.optional !== undefined && node.optional !== false && node.optional !== null) {
|
|
5983
|
+
out.question = node.optional === '-' ? '-' : node.optional === '+' ? '+' : '?';
|
|
5984
|
+
}
|
|
5985
|
+
if (node.readonly !== undefined && node.readonly !== false && node.readonly !== null) {
|
|
5986
|
+
out.readonly = node.readonly === '-' ? '-' : node.readonly === '+' ? '+' : 'readonly';
|
|
5987
|
+
}
|
|
5988
|
+
return out;
|
|
5989
|
+
}
|
|
5990
|
+
case 'TSTypeAnnotation':
|
|
5991
|
+
return toSourceBabelTS(node.typeAnnotation);
|
|
5992
|
+
case 'TSTypeLiteral':
|
|
5993
|
+
{
|
|
5994
|
+
const _properties = {};
|
|
5995
|
+
let indexSignatures;
|
|
5996
|
+
for (const member of (_node$members = node.members) != null ? _node$members : []) {
|
|
5997
|
+
var _node$members;
|
|
5998
|
+
if (member.type === 'TSIndexSignature') {
|
|
5999
|
+
var _indexSignatures;
|
|
6000
|
+
indexSignatures = (_indexSignatures = indexSignatures) != null ? _indexSignatures : [];
|
|
6001
|
+
indexSignatures.push(toSourceBabelTS(member));
|
|
6002
|
+
} else if (member.type === 'TSPropertySignature') {
|
|
6003
|
+
const name = toSourceBabelTS(member.key);
|
|
6004
|
+
let type = toSourceBabelTS(member.typeAnnotation);
|
|
6005
|
+
if (member.optional) {
|
|
6006
|
+
if (type && typeof type === 'object') type.optional = true;else type = {
|
|
6007
|
+
type,
|
|
6008
|
+
optional: true
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
6011
|
+
if (member.readonly) {
|
|
6012
|
+
if (type && typeof type === 'object') type.readonly = true;else type = {
|
|
6013
|
+
type,
|
|
6014
|
+
readonly: true
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
6017
|
+
_properties[name] = type;
|
|
6018
|
+
} else {
|
|
6019
|
+
console.warn('TSTypeLiteral: unhandled member', member.type);
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
const ret = {
|
|
6023
|
+
type: 'object'
|
|
6024
|
+
};
|
|
6025
|
+
if (Object.keys(_properties).length) {
|
|
6026
|
+
ret.properties = _properties;
|
|
6027
|
+
}
|
|
6028
|
+
if (indexSignatures) {
|
|
6029
|
+
ret.indexSignatures = indexSignatures;
|
|
6030
|
+
}
|
|
6031
|
+
return ret;
|
|
6032
|
+
}
|
|
6033
|
+
case 'TSIndexSignature':
|
|
6034
|
+
{
|
|
6035
|
+
var _node$parameters;
|
|
6036
|
+
const indexType = toSourceBabelTS(node.typeAnnotation);
|
|
6037
|
+
const indexParameters = ((_node$parameters = node.parameters) != null ? _node$parameters : []).map(toSourceBabelTS);
|
|
6038
|
+
return {
|
|
6039
|
+
type: 'indexSignature',
|
|
6040
|
+
indexType,
|
|
6041
|
+
indexParameters
|
|
6042
|
+
};
|
|
6043
|
+
}
|
|
4882
6044
|
case 'TSTypeOperator':
|
|
4883
6045
|
if (node.operator === 'readonly') {
|
|
4884
6046
|
// readonly erased at runtime, same shape as the inner type.
|
|
4885
6047
|
return toSourceBabelTS(node.typeAnnotation);
|
|
4886
6048
|
}
|
|
6049
|
+
if (node.operator === 'keyof') {
|
|
6050
|
+
const keyofArg = toSourceBabelTS(node.typeAnnotation);
|
|
6051
|
+
return {
|
|
6052
|
+
type: 'keyof',
|
|
6053
|
+
argument: keyofArg
|
|
6054
|
+
};
|
|
6055
|
+
}
|
|
4887
6056
|
console.warn('unimplemented TSTypeOperator', node.operator);
|
|
4888
6057
|
return 'any';
|
|
4889
6058
|
case 'TSQualifiedName':
|
|
@@ -5014,6 +6183,8 @@ class JSDocAnnotator {
|
|
|
5014
6183
|
this.parents = [];
|
|
5015
6184
|
/** @type {Record<string, object>} */
|
|
5016
6185
|
this.typedefs = {};
|
|
6186
|
+
/** @type {Record<string, string[]>} */
|
|
6187
|
+
this.typedefTemplates = {};
|
|
5017
6188
|
/** @type {import('./parseJSDoc.js').ExpandType} */
|
|
5018
6189
|
this.expandType = options.expandType || expandTypeDepFree;
|
|
5019
6190
|
}
|
|
@@ -5030,7 +6201,7 @@ class JSDocAnnotator {
|
|
|
5030
6201
|
if (comments) {
|
|
5031
6202
|
for (const comment of comments) {
|
|
5032
6203
|
const warn = console.warn.bind(console);
|
|
5033
|
-
parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
|
|
6204
|
+
parseJSDocTypedef(this.typedefs, this.typedefTemplates, warn, comment, this.expandType);
|
|
5034
6205
|
}
|
|
5035
6206
|
}
|
|
5036
6207
|
this.traverse(ast.program);
|