@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.
Files changed (3) hide show
  1. package/index.cjs +1522 -296
  2. package/index.mjs +1419 -288
  3. 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 === ts.SyntaxKind.MappedType
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
- return {
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 (const c of str) {
706
- 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--;
707
- if (c === delimiter && depthAngle === 0 && depthCurly === 0 && depthSquare === 0 && depthParen === 0) {
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 firstComma = recordSlice.indexOf(',');
827
- if (firstComma === -1) {
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 propertiesArray = type.slice(1, -1).split(','); // ['entity: Entity', ' app: AppBase']
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
- propertiesArray.forEach(_ => {
876
- const [propName, propType] = _.split(":").map(_ => _.trim());
877
- if (!propName || !propType) {
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
- return false;
1258
+ continue;
880
1259
  }
881
- properties[propName] = propType;
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) expand unions
889
- const members = type.split("|");
890
- if (members.length >= 2) {
891
- members.forEach((_, i) => members[i] = _.trim());
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: members.map(expandTypeDepFree)
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
- // (6) expand [] Arrays
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
- // (7) expand tuples
1325
+ // (10) expand tuples
907
1326
  if (type[0] === '[' && type[type.length - 1] === ']') {
908
- const elements = type.slice(1, -1).split(','); // ['null', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture']
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
- // (8) expand typeof expressions
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
- * Infers a parameter type from its default value AST node.
944
- * Returns widened types like TypeScript does (`= 0` means `number`, not
945
- * literal `0`; `= null` widens to `any`).
946
- * Returns `undefined` when nothing useful can be inferred — the caller then
947
- * emits no check, exactly like an undocumented parameter today.
948
- * Shapes match what `expandType` produces so they can be embedded as-is.
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
- content,
1075
- nextIndex: k + 1
2092
+ type: 'any',
2093
+ raw: undefined,
2094
+ jsdoc: false
1076
2095
  };
1077
2096
  }
1078
2097
  /**
1079
- * Parses JSDoc comments to extract and expand typedefs and their associated properties.
1080
- *
1081
- * It iterates through the lines of a `CommentBlock` from the Babel AST, looking for `@typedef` and `@property`
1082
- * annotations. When it finds a typedef, it stores it in the `typedefs` record. When it finds a property,
1083
- * it adds it to the last found typedef if it is an object type.
1084
- * @param {Record<string, object>} typedefs - An object to store typedefs, mapping type names to their expanded definitions.
1085
- * @param {Console["warn"]} warn - A warn function used for emitting warnings about non-extensible types.
1086
- * @param {import("@babel/types").Comment} comment - A comment extracted from Babel's AST, expected to be a CommentBlock containing type definitions.
1087
- * @param {Function} expandType - A function that takes a type expression as a string and returns a structured representation of the type.
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 parseJSDocTypedef(typedefs, warn, comment, expandType) {
1090
- const {
1091
- type,
1092
- value
1093
- } = comment;
1094
- if (type !== 'CommentBlock') {
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 lines = value.split('\n');
1098
- let lastTypedef;
1099
- for (let line of lines) {
1100
- line = line.trim();
1101
- if (line[0] === '*') {
1102
- line = line.slice(1).trim();
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
- if (line.startsWith('@typedef')) {
1105
- const {
1106
- content: def,
1107
- nextIndex
1108
- } = extractCurlyContent(line);
1109
- let name = line.substring(nextIndex).trim();
1110
- // Drop description
1111
- name = name.split(' ')[0];
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
- } else if (line.startsWith('@property')) {
1118
- var _lastTypedef;
1119
- // class @property
1120
- if (!lastTypedef) {
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
- const {
1124
- content,
1125
- nextIndex
1126
- } = extractCurlyContent(line);
1127
- const rest = line.substring(nextIndex);
1128
- const propType = expandType(content);
1129
- const [name, optional] = extractNameAndOptionality(rest);
1130
- // console.log({name, optional, propType});
1131
- const finalType = annotateOptional(propType, optional);
1132
- if (((_lastTypedef = lastTypedef) == null ? void 0 : _lastTypedef.type) === 'object') {
1133
- lastTypedef.properties[name] = finalType;
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
- } else if (line.startsWith('@callback')) {
1138
- const name = line.substring(9).trim();
1139
- typedefs[name] = 'Function';
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
- return templates;
1319
- }
1320
-
1321
- function _extends() {
1322
- _extends = Object.assign ? Object.assign.bind() : function (target) {
1323
- for (var i = 1; i < arguments.length; i++) {
1324
- var source = arguments[i];
1325
- for (var key in source) {
1326
- if (Object.prototype.hasOwnProperty.call(source, key)) {
1327
- target[key] = source[key];
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
- return target;
1332
- };
1333
- return _extends.apply(this, arguments);
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
- * Strips empty `properties` and collapses empty `object` types to the bare
1359
- * string `'object'`. Numbers/booleans (literal types) pass through.
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
- out += `registerTypedef('${name}', ${json});\n`;
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
- // console.log(`node.typeName.name=${node.typeName.name} name=${name}`, node);
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);