@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.
Files changed (3) hide show
  1. package/index.cjs +1551 -282
  2. package/index.mjs +1450 -279
  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,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 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
+ }
883
1268
  return {
884
1269
  type: 'object',
885
1270
  properties
886
1271
  };
887
1272
  }
888
- // (5) expand unions
889
- const members = type.split("|");
890
- if (members.length >= 2) {
891
- members.forEach((_, i) => members[i] = _.trim());
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: members.map(expandTypeDepFree)
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
- // (6) expand [] Arrays
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
- // (7) expand tuples
1318
+ // (10) expand tuples
907
1319
  if (type[0] === '[' && type[type.length - 1] === ']') {
908
- const elements = type.slice(1, -1).split(','); // ['null', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture', ' Texture']
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
- // (8) expand typeof expressions
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
- * 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
- };
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
- content,
1075
- nextIndex: k + 1
2085
+ type: 'any',
2086
+ raw: undefined,
2087
+ jsdoc: false
1076
2088
  };
1077
2089
  }
1078
2090
  /**
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.
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 parseJSDocTypedef(typedefs, warn, comment, expandType) {
1090
- const {
1091
- type,
1092
- value
1093
- } = comment;
1094
- if (type !== 'CommentBlock') {
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 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();
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
- 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;
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
- } else if (line.startsWith('@property')) {
1118
- var _lastTypedef;
1119
- // class @property
1120
- if (!lastTypedef) {
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
- 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);
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
- } else if (line.startsWith('@callback')) {
1138
- const name = line.substring(9).trim();
1139
- typedefs[name] = 'Function';
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
- 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
- }
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
- return target;
1332
- };
1333
- return _extends.apply(this, arguments);
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
- out += `registerTypedef('${name}', ${json});\n`;
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
- // console.log(`node.typeName.name=${node.typeName.name} name=${name}`, node);
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);