@astryxdesign/cli 0.7.2-canary.e2909cf → 0.7.2-canary.e4bdd70

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 (194) hide show
  1. package/README.md +15 -14
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +41 -1
  4. package/api/component/_adapter.mjs +93 -7
  5. package/api/component/component.mjs +22 -5
  6. package/api/component/integration-export-name.test.mjs +644 -0
  7. package/api/docs/_adapter.d.mts +11 -0
  8. package/api/docs/_adapter.mjs +244 -1
  9. package/api/docs/integrationDocs.test.mjs +124 -0
  10. package/api/doctor/theme-checks.mjs +1 -1
  11. package/api/gap-report/gap-report.mjs +67 -4
  12. package/api/index.d.mts +2 -0
  13. package/api/index.mjs +2 -0
  14. package/api/init/run/run.mjs +2 -2
  15. package/api/integration/_adapter.d.mts +12 -0
  16. package/api/integration/_adapter.mjs +54 -0
  17. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  18. package/api/integration/add-contribution.mjs +8 -4
  19. package/api/integration/add-contribution.test.mjs +4 -2
  20. package/api/integration/authoring-checks.mjs +11 -2
  21. package/api/integration/authoring-checks.test.mjs +39 -0
  22. package/api/integration/authoring-checks.type.d.mts +9 -0
  23. package/api/integration/authoring-checks.type.mjs +14 -1
  24. package/api/integration/check/check.d.mts +96 -0
  25. package/api/integration/check/check.mjs +357 -0
  26. package/api/integration/check/check.test.mjs +489 -0
  27. package/api/integration/integration-check.type.d.mts +118 -0
  28. package/api/integration/integration-check.type.mjs +84 -0
  29. package/api/integration/integrationCheck.doc.d.mts +10 -0
  30. package/api/integration/integrationCheck.doc.mjs +73 -0
  31. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  32. package/api/integration/pack-check.mjs +51 -97
  33. package/api/integration/pack-check.test.mjs +359 -0
  34. package/api/integration/validate-integration.mjs +8 -0
  35. package/api/integration/validate-integration.test.mjs +11 -0
  36. package/api/integration/validate-integration.type.d.mts +8 -0
  37. package/api/integration/validate-integration.type.mjs +12 -1
  38. package/api/integration/validateIntegration.doc.mjs +17 -3
  39. package/api/search/search.d.mts +22 -6
  40. package/api/search/search.domain-priority.test.mjs +93 -18
  41. package/api/search/search.integration-check.test.mjs +24 -0
  42. package/api/search/search.mjs +133 -22
  43. package/api/swizzle/copy/copy.mjs +28 -14
  44. package/api/theme/build/build.family.test.mjs +54 -6
  45. package/api/theme/build/build.mjs +114 -17
  46. package/api/theme/themeBuild.doc.mjs +4 -4
  47. package/assets/docs/README.md +10 -0
  48. package/assets/docs/author-a-theme.doc.mjs +1 -1
  49. package/assets/docs/getting-started.doc.mjs +3 -3
  50. package/assets/docs/styling-overview.doc.mjs +1 -1
  51. package/assets/docs/theme.doc.dense.mjs +1 -1
  52. package/assets/docs/theme.doc.mjs +1 -1
  53. package/assets/docs/theme.doc.zh.mjs +1 -1
  54. package/assets/docs/tree/add-a-component.doc.mjs +11 -1
  55. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  56. package/assets/docs/tree/add-a-topic.doc.mjs +10 -7
  57. package/assets/docs/tree/agent-guidance.doc.mjs +2 -2
  58. package/assets/docs/tree/block-template.doc.mjs +8 -20
  59. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  60. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  61. package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
  62. package/assets/docs/tree/checks.doc.mjs +24 -16
  63. package/assets/docs/tree/codemods.doc.mjs +4 -4
  64. package/assets/docs/tree/component-family.doc.mjs +8 -6
  65. package/assets/docs/tree/component-imports.doc.mjs +10 -2
  66. package/assets/docs/tree/components.doc.mjs +1 -1
  67. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +7 -18
  68. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  69. package/assets/docs/tree/describe-the-component.doc.mjs +2 -2
  70. package/assets/docs/tree/docs.doc.mjs +3 -3
  71. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  72. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  73. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  74. package/assets/docs/tree/extend-or-replace.doc.mjs +6 -6
  75. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  76. package/assets/docs/tree/generate-a-palette.doc.mjs +5 -3
  77. package/assets/docs/tree/grade-template-with-agent.doc.mjs +1 -1
  78. package/assets/docs/tree/integrations.doc.mjs +8 -0
  79. package/assets/docs/tree/links.doc.mjs +6 -5
  80. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  81. package/assets/docs/tree/page-template.doc.mjs +4 -8
  82. package/assets/docs/tree/publishing.doc.mjs +1 -1
  83. package/assets/docs/tree/quick-start.doc.mjs +37 -21
  84. package/assets/docs/tree/replace-a-core-component.doc.mjs +4 -6
  85. package/assets/docs/tree/replace-a-core-template.doc.mjs +6 -8
  86. package/assets/docs/tree/sections-and-placement.doc.mjs +4 -6
  87. package/assets/docs/tree/see-it-in-an-app.doc.mjs +4 -2
  88. package/assets/docs/tree/short-and-findable.doc.mjs +6 -5
  89. package/assets/docs/tree/single-component.doc.mjs +15 -6
  90. package/assets/docs/tree/start-a-template.doc.mjs +3 -3
  91. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  92. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  93. package/assets/docs/tree/template-doc-overview.doc.mjs +4 -15
  94. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  95. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  96. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  97. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  98. package/assets/docs/tree/templates.doc.mjs +1 -1
  99. package/assets/docs/tree/test-in-an-app.doc.mjs +3 -3
  100. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  101. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  102. package/assets/docs/tree/troubleshooting.doc.mjs +13 -5
  103. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  104. package/assets/docs/tree/upgrading.doc.mjs +3 -3
  105. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +3 -1
  106. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  107. package/assets/docs/tree/versioning.doc.mjs +7 -6
  108. package/assets/docs/tree/write-the-template-file.doc.mjs +4 -3
  109. package/assets/docs/use-a-theme.doc.mjs +2 -2
  110. package/assets/theme.template.ts +1 -1
  111. package/authoring/codemod/codemod.doc.mjs +7 -3
  112. package/authoring/config/config.doc.mjs +9 -6
  113. package/authoring/config/type.ts +4 -2
  114. package/authoring/discover/discover.doc.mjs +1 -1
  115. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  116. package/authoring/doctypes/command/command.doc.mjs +14 -12
  117. package/authoring/doctypes/component/component.doc.mjs +29 -26
  118. package/authoring/doctypes/component/type.ts +13 -8
  119. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  120. package/authoring/doctypes/function/function.doc.mjs +9 -8
  121. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  122. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  123. package/authoring/doctypes/reference/reference.doc.mjs +11 -10
  124. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  125. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  126. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  127. package/authoring/doctypes/template/parse.d.mts +6 -2
  128. package/authoring/doctypes/template/parse.mjs +3 -1
  129. package/authoring/doctypes/template/template.doc.mjs +3 -3
  130. package/authoring/doctypes/template/type.ts +22 -0
  131. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  132. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  133. package/authoring/identity/identity.doc.mjs +3 -3
  134. package/authoring/index.d.ts +1 -0
  135. package/authoring/integration/integration.doc.mjs +12 -4
  136. package/authoring/integration/parse.test.mjs +10 -0
  137. package/authoring/integration/schema.d.mts +2 -0
  138. package/authoring/integration/schema.mjs +1 -0
  139. package/authoring/integration/type.ts +4 -0
  140. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  141. package/clients/cli/commands/build-theme.mjs +1 -1
  142. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  143. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  144. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  145. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  146. package/clients/cli/commands/doctor.mjs +190 -0
  147. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  148. package/clients/cli/commands/integration-check.test.mjs +267 -0
  149. package/clients/cli/commands/integration.doc.mjs +6 -2
  150. package/clients/cli/commands/integration.mjs +107 -1
  151. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  152. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  153. package/clients/cli/index.mjs +1 -0
  154. package/foundation/config/project.mjs +82 -0
  155. package/foundation/discovery/authoring-self-docs.mjs +2 -1
  156. package/foundation/discovery/component-discovery.d.mts +178 -10
  157. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  158. package/foundation/discovery/component-discovery.mjs +659 -62
  159. package/foundation/discovery/component-replacement.mjs +18 -0
  160. package/foundation/discovery/component-replacement.test.mjs +40 -0
  161. package/foundation/discovery/doc-types.d.mts +157 -0
  162. package/foundation/discovery/doc-types.mjs +1224 -0
  163. package/foundation/discovery/doc-types.test.mjs +652 -0
  164. package/foundation/discovery/docs-discovery.d.mts +81 -0
  165. package/foundation/discovery/docs-discovery.mjs +57 -0
  166. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  167. package/foundation/discovery/integration-doc-files.mjs +231 -0
  168. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  169. package/foundation/discovery/module-exports.d.mts +27 -0
  170. package/foundation/discovery/module-exports.mjs +356 -31
  171. package/foundation/discovery/module-exports.test.mjs +172 -0
  172. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  173. package/foundation/discovery/package-tsconfig.mjs +228 -0
  174. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  175. package/foundation/discovery/project-typescript.d.mts +23 -0
  176. package/foundation/discovery/project-typescript.mjs +61 -0
  177. package/foundation/discovery/prop-drift.d.mts +278 -0
  178. package/foundation/discovery/prop-drift.mjs +2263 -0
  179. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  180. package/foundation/discovery/type-match.d.mts +53 -0
  181. package/foundation/discovery/type-match.mjs +660 -0
  182. package/foundation/discovery/type-match.test.mjs +169 -0
  183. package/foundation/discovery/type-text.d.mts +68 -0
  184. package/foundation/discovery/type-text.mjs +658 -0
  185. package/foundation/discovery/type-text.test.mjs +196 -0
  186. package/foundation/discovery/unresolved-references.d.mts +62 -0
  187. package/foundation/discovery/unresolved-references.mjs +790 -0
  188. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  189. package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
  190. package/foundation/doc-compiler/links.d.mts +9 -5
  191. package/foundation/doc-compiler/links.mjs +4 -2
  192. package/foundation/integrations/contribution-inventory.mjs +6 -1
  193. package/foundation/response/response-types.doc.mjs +7 -2
  194. package/package.json +9 -9
@@ -0,0 +1,2263 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Compare a documented prop table with the props the TypeScript source
5
+ * declares for the export it describes.
6
+ *
7
+ * The compiler is the project's own `typescript`. Without one it can use, the
8
+ * comparison is skipped and says why. Every finding is a warning.
9
+ *
10
+ * `comparePropDocs` finds each component doc's prop tables first: a doc's
11
+ * own `props` document the export its `name` names; each entry of a
12
+ * multi-component doc documents its own export, which a member record
13
+ * compares against its own source. Hook entries (`use…`) document parameters
14
+ * and returns, not props, and are not compared.
15
+ *
16
+ * Each table (a doc's `props`, or a multi-component doc's entry, with the
17
+ * export it documents and the files that may hold it) is compared with that
18
+ * export's props type:
19
+ * - `not_in_code`: a documented prop the props type does not have (a
20
+ * `data-*`-style name an index signature accepts counts as present);
21
+ * - `not_in_doc`: a prop the package's own source declares, or builds with a
22
+ * mapped type from its own keys, that the table does not list. Props from
23
+ * outside the package (React, the DOM, another package's types or keys) are
24
+ * never required, and neither are `className`, `style`, `xstyle`,
25
+ * `data-testid`, `ref`, `key`, or a prop whose declaration carries an
26
+ * `internal` JSDoc tag;
27
+ * - `required_mismatch`: `required` disagrees with whether every caller must
28
+ * pass the prop (one only some members of the props type require, or that
29
+ * the package's `defaultProps` fills, is optional, as it is to JSX
30
+ * callers; an entry that names several props claims its `required` for
31
+ * each);
32
+ * - `type_mismatch`: for a prop declared in the package, the documented type
33
+ * differs from the code's where both sides read exactly (type-match.mjs
34
+ * states the rules); every other type is listed as not compared.
35
+ *
36
+ * @input Component records (name, docPath, sourcePath), or prop tables, the
37
+ * package root, and the directories to resolve `typescript` from.
38
+ * @output A PropDriftReport.
39
+ * @position Shared by `astryx doctor integration components` and the Astryx
40
+ * repository's test over Core's own component docs.
41
+ */
42
+
43
+ import * as fs from 'node:fs';
44
+ import * as path from 'node:path';
45
+ import {folderSourceFor} from './component-discovery.mjs';
46
+ import {loadComponentDoc} from './component-loader.mjs';
47
+ import {readPackageTsconfig} from './package-tsconfig.mjs';
48
+ import {
49
+ resolveProjectTypeScript,
50
+ unusableTypeScriptReason,
51
+ } from './project-typescript.mjs';
52
+ import {judgeType} from './type-match.mjs';
53
+ import {typeTextTooOld} from './type-text.mjs';
54
+ import {
55
+ unresolvedGlobals,
56
+ unresolvedReference,
57
+ } from './unresolved-references.mjs';
58
+
59
+ /**
60
+ * @typedef {'not_in_code' | 'not_in_doc' | 'required_mismatch' | 'type_mismatch'} PropDriftKind
61
+ */
62
+
63
+ /**
64
+ * @typedef {object} PropDriftFinding
65
+ * @property {string} component the documented component (export) name
66
+ * @property {string} prop
67
+ * @property {PropDriftKind} kind
68
+ * @property {'warning'} severity
69
+ * @property {string} message
70
+ * @property {string} doc the doc file, relative to the package root
71
+ * @property {string} source the source file, relative to the package root
72
+ * @property {string} [docType] the documented type, as written
73
+ * @property {string} [codeType] the type as the source declares it
74
+ * @property {boolean} [docRequired]
75
+ * @property {boolean} [codeRequired]
76
+ */
77
+
78
+ /**
79
+ * @typedef {object} PropDriftUnchecked
80
+ * @property {string} component
81
+ * @property {string} doc the doc file, relative to the package root
82
+ * @property {string} reason why its props could not be read
83
+ */
84
+
85
+ /**
86
+ * @typedef {object} PropDriftTypesNotCompared
87
+ * @property {string} component
88
+ * @property {string} doc the doc file, relative to the package root
89
+ * @property {string} reason why: not a form the check judges exactly, a
90
+ * field the prop's variants give different types, a standard prop
91
+ * (`className`, `style`, …), or a prop declared outside the package
92
+ * @property {string[]} props documented props whose types were not compared
93
+ */
94
+
95
+ /**
96
+ * @typedef {object} PropDriftReport
97
+ * @property {'checked' | 'skipped'} status `skipped` means nothing was
98
+ * compared, and `reason` says why
99
+ * @property {string} [reason]
100
+ * @property {string} [typescript] the version of the compiler that read the props
101
+ * @property {number} components how many documented prop tables were compared
102
+ * @property {number} typesCompared how many documented prop types were judged
103
+ * @property {string} [typesSkipped] why no prop type was judged
104
+ * @property {PropDriftFinding[]} findings
105
+ * @property {PropDriftUnchecked[]} unchecked documented components whose props
106
+ * could not be read from the source
107
+ * @property {Array<PropDriftUnchecked & {props: string[]}>} [propsNotChecked]
108
+ * props a mapped type builds from keys the check can't trace, which a doc
109
+ * leaving out draws no warning for
110
+ * @property {PropDriftTypesNotCompared[]} typesNotCompared every other
111
+ * documented prop type, grouped by why it was not judged
112
+ */
113
+
114
+ /**
115
+ * @typedef {object} PropDriftTable
116
+ * @property {string} doc the doc file, relative to the package root
117
+ * @property {string} name the export the table documents
118
+ * @property {any[]} props the table's entries
119
+ * @property {string[]} sources the files that may hold the export, in order
120
+ */
121
+
122
+ /**
123
+ * @typedef {object} PropDriftComponent
124
+ * @property {string} name
125
+ * @property {string} docPath absolute path to the component's `.doc.*` file
126
+ * @property {string | null} sourcePath absolute path to its same-stem
127
+ * source, or null when no file in the package declares it (a family member
128
+ * only another folder or built output defines)
129
+ */
130
+
131
+ /**
132
+ * Props a doc never needs to list: the internal and styling props the
133
+ * `ComponentPropDoc` authoring guidance says to skip, `ref`, which Core's
134
+ * docs also leave out, and `key`, which React keeps for itself.
135
+ */
136
+ export const PROPS_NOT_DOCUMENTED = Object.freeze([
137
+ 'className',
138
+ 'style',
139
+ 'xstyle',
140
+ 'data-testid',
141
+ 'ref',
142
+ 'key',
143
+ ]);
144
+
145
+ /** The guidance's internal props: a declaration tagged `@internal`. */
146
+ const SKIP_TAGS = new Set(['internal']);
147
+ /** Why a documented prop type was not judged (`typesNotCompared`). */
148
+ const NOT_EXACT = 'not a form the check judges exactly';
149
+ /** A field documented with one variant's type, where they differ. */
150
+ const VARIANTS = "the prop's variants give this field different types";
151
+ /** @type {(name: string, code?: CodeProp) => string} why a prop is not judged */
152
+ const outside = (name, code) =>
153
+ code?.untraced
154
+ ? NOT_EXACT
155
+ : PROPS_NOT_DOCUMENTED.includes(name)
156
+ ? 'standard prop, not compared'
157
+ : 'declared outside the package, not compared';
158
+ const HOOK_NAME = /^use[A-Z0-9]/;
159
+
160
+ /** The line both surfaces print when the compiler is missing. */
161
+ export const TYPESCRIPT_MISSING_REASON =
162
+ 'TypeScript is not installed in this project, so documented props were not compared with the code. Install typescript and run this check again.';
163
+
164
+ /**
165
+ * @template T
166
+ * @param {() => T} compute
167
+ * @returns {() => T}
168
+ */
169
+ function memo(compute) {
170
+ /** @type {{value: T} | null} */
171
+ let cached = null;
172
+ return () => {
173
+ if (cached == null) cached = {value: compute()};
174
+ return cached.value;
175
+ };
176
+ }
177
+
178
+ /** @type {WeakMap<Function, (file: string) => boolean>} */
179
+ const MINE = new WeakMap();
180
+
181
+ /**
182
+ * Whose type names the walk checks: the package's own files and any that
183
+ * aren't a dependency's. The same test for the same own-file test.
184
+ * @param {(file: string) => boolean} isOwnFile
185
+ */
186
+ function mineFor(isOwnFile) {
187
+ let mine = MINE.get(isOwnFile);
188
+ if (!mine) {
189
+ mine = (/** @type {string} */ file) =>
190
+ isOwnFile(file) || !/[\\/]node_modules[\\/]/.test(file);
191
+ MINE.set(isOwnFile, mine);
192
+ }
193
+ return mine;
194
+ }
195
+
196
+ /**
197
+ * A memoized "is this file part of the package's own source?" test.
198
+ * @param {string} root realpath of the package root
199
+ */
200
+ function ownFileTest(root) {
201
+ /** @type {Map<string, boolean>} */
202
+ const cache = new Map();
203
+ return (/** @type {string} */ file) => {
204
+ let own = cache.get(file);
205
+ if (own !== undefined) return own;
206
+ let real = file;
207
+ try {
208
+ real = fs.realpathSync(file);
209
+ } catch {
210
+ // A lib or virtual file: compare as given.
211
+ }
212
+ const rel = path.relative(root, real);
213
+ own =
214
+ rel !== '' &&
215
+ !rel.startsWith('..') &&
216
+ !path.isAbsolute(rel) &&
217
+ !rel.split(path.sep).includes('node_modules');
218
+ cache.set(file, own);
219
+ return own;
220
+ };
221
+ }
222
+
223
+ /**
224
+ * @typedef {object} CodeProp
225
+ * @property {string} name
226
+ * @property {boolean} required whether every caller must pass it: false
227
+ * when a member of the props type, an overload, or the package's
228
+ * `defaultProps` lets a caller leave it out
229
+ * @property {boolean} own declared in the package's own source, or built
230
+ * from keys it writes
231
+ * @property {boolean} [untraced] built from keys the check can't trace
232
+ * @property {boolean} skipTagged its declaration carries an internal JSDoc tag
233
+ * @property {string[]} declared the type as written, per own declaration
234
+ * @property {() => string[][]} renderings the compiler's renderings, computed on demand
235
+ * @property {() => any[]} types the prop's type in each member of the props type
236
+ * @property {boolean} intersected one member declares it more than once (an
237
+ * intersection), so no one declaration's text is its type
238
+ * @property {() => any[] | null} indexed the type an index signature gives
239
+ * the name in each member that lacks the prop; null when two signatures
240
+ * in one member take it
241
+ * @property {any} location where names in the prop's type resolve
242
+ */
243
+
244
+ /**
245
+ * Read the props of one export.
246
+ * @param {any} ts
247
+ * @param {any} program
248
+ * @param {any} checker
249
+ * @param {string} sourcePath
250
+ * @param {string} exportName
251
+ * @param {(file: string) => boolean} isOwnFile
252
+ * @param {import('./unresolved-references.mjs').UnresolvedReference | null} ambient
253
+ * what the program's other roots cannot resolve, which any component may
254
+ * use
255
+ * @returns {{ok: true, props: Map<string, CodeProp>, accepts: (name: string) => any} | {ok: false, reason: string}}
256
+ */
257
+ function readCodeProps(
258
+ ts,
259
+ program,
260
+ checker,
261
+ sourcePath,
262
+ exportName,
263
+ isOwnFile,
264
+ ambient,
265
+ ) {
266
+ const sourceFile = program.getSourceFile(sourcePath);
267
+ const sourceName = path.basename(sourcePath);
268
+ if (!sourceFile) {
269
+ return {ok: false, reason: `the compiler could not read ${sourceName}`};
270
+ }
271
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
272
+ const exports = moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [];
273
+ const stem = sourceName.replace(/\.[cm]?[jt]sx?$/, '');
274
+ let symbol =
275
+ exports.find(/** @param {any} s */ s => s.name === exportName) ??
276
+ (stem === exportName
277
+ ? exports.find(/** @param {any} s */ s => s.name === 'default')
278
+ : undefined);
279
+ if (!symbol) {
280
+ return {
281
+ ok: false,
282
+ reason: `${sourceName} has no export named ${exportName}`,
283
+ };
284
+ }
285
+ if (symbol.flags & ts.SymbolFlags.Alias) {
286
+ symbol = checker.getAliasedSymbol(symbol);
287
+ }
288
+ // An import that does not resolve, in the files the component comes from,
289
+ // can make any part of its props a guess, whatever the code looks like.
290
+ // Some TypeScript versions read such a name as a plain `any`, so this comes
291
+ // first.
292
+ const own = [sourceFile, ...(symbol.declarations ?? [])]
293
+ .map(/** @param {any} n */ n => n.getSourceFile())
294
+ .filter(/** @param {any} sf */ sf => isOwnFile(sf.fileName));
295
+ // Type names are checked in the files that aren't a dependency's. One
296
+ // test per run, so the walk reads each file once for every component.
297
+ const mine = mineFor(isOwnFile);
298
+ const found = unresolvedReference(
299
+ ts,
300
+ program,
301
+ checker,
302
+ own,
303
+ () => true,
304
+ mine,
305
+ );
306
+ // A reference that doesn't resolve names what to install; a type the
307
+ // compiler can't give in the component's own files may only show a broken
308
+ // global one.
309
+ const missing = found && found.how !== 'used' ? found : (ambient ?? found);
310
+ if (missing) {
311
+ const where =
312
+ path.relative(path.dirname(sourcePath), missing.file) || sourceName;
313
+ return {
314
+ ok: false,
315
+ reason:
316
+ missing.how === 'used'
317
+ ? `${exportName} depends on ${missing.name} (used in ${where}), a type the compiler can't work out, so its props would be a guess. Check that the package's dependencies are installed and its types compile, then run this check again`
318
+ : `${exportName} depends on ${missing.name} (${missing.how} in ${where}), which the compiler could not resolve, usually a dependency that is not installed. Install the package's dependencies and run this check again`,
319
+ };
320
+ }
321
+ const type = checker.getTypeOfSymbolAtLocation(symbol, sourceFile);
322
+ // Each member of a union (`ComponentType<P>`: a class or a function).
323
+ const signatures = (type.isUnion() ? type.types : [type]).flatMap(
324
+ /** @param {any} t */ t => [
325
+ ...t.getCallSignatures(),
326
+ ...t.getConstructSignatures(),
327
+ ],
328
+ );
329
+ if (signatures.length === 0) {
330
+ // An `any` the source does not write comes from a type that does not
331
+ // resolve, such as `forwardRef` when React's types are not installed.
332
+ const writtenAny =
333
+ symbol.valueDeclaration?.type?.kind === ts.SyntaxKind.AnyKeyword;
334
+ const unresolved =
335
+ !writtenAny &&
336
+ (type.flags & ts.TypeFlags.Any ||
337
+ hasUnresolvedPart(ts, checker, type, 0, new Set()));
338
+ return {
339
+ ok: false,
340
+ reason: unresolved
341
+ ? `${exportName}'s own type includes a type the compiler could not resolve, usually an import whose package is not installed (such as React's types, for a forwardRef or memo component). Install the package's dependencies and run this check again`
342
+ : writtenAny
343
+ ? `${exportName} is typed \`any\`, so it has no props to compare`
344
+ : `${exportName} in ${sourceName} is not a component (it cannot be called with props)`,
345
+ };
346
+ }
347
+ const unresolved = {
348
+ ok: /** @type {const} */ (false),
349
+ reason: `${exportName}'s props type includes a type the compiler could not resolve, usually an import whose package is not installed. Install the package's dependencies and run this check again`,
350
+ };
351
+ // Every overload: its props type joins the others as a member, as a
352
+ // union's members do. An overload that takes no props makes every prop
353
+ // optional.
354
+ /** @type {any[]} */
355
+ const propsTypes = [];
356
+ let propless = false;
357
+ for (const signature of signatures) {
358
+ const parameter = signature.getParameters()[0];
359
+ if (parameter) {
360
+ propsTypes.push(checker.getTypeOfSymbolAtLocation(parameter, sourceFile));
361
+ } else {
362
+ propless = true;
363
+ }
364
+ }
365
+ /** @type {Map<string, CodeProp>} */
366
+ const props = new Map();
367
+ if (propsTypes.length === 0) return {ok: true, props, accepts: () => []};
368
+
369
+ // Props that are `any`, written so or made from it (`FC<any>`, or
370
+ // `forwardRef<E, any>`'s `Omit<any, 'ref'>`).
371
+ if (propsTypes.some(propsType => isAnyProps(ts, checker, propsType))) {
372
+ return {
373
+ ok: false,
374
+ reason: `${exportName}'s props are typed \`any\`, so there is nothing to compare them with`,
375
+ };
376
+ }
377
+ if (
378
+ propsTypes.some(propsType =>
379
+ hasUnresolvedPart(ts, checker, propsType, 0, new Set()),
380
+ )
381
+ ) {
382
+ return unresolved;
383
+ }
384
+
385
+ const members = propsTypes
386
+ .flatMap(propsType => (propsType.isUnion() ? propsType.types : [propsType]))
387
+ .filter(
388
+ /** @param {any} t */
389
+ t => !(t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)),
390
+ );
391
+ // A generic component whose props take their names from a type parameter
392
+ // (the element it renders: `Omit<ComponentPropsWithoutRef<E>, 'as'>`, or a
393
+ // bare `props: P`) accepts names that are known only where it is used.
394
+ const generic = signatures.some(
395
+ /** @param {any} signature */ signature =>
396
+ (signature.getTypeParameters?.() ?? []).length > 0,
397
+ );
398
+ // Props that refer to a type parameter and show the compiler no members
399
+ // at all would draw a finding for every documented name: listed too.
400
+ const unnamed =
401
+ members.some(member => isDeferred(ts, checker, member, true)) &&
402
+ members.every(
403
+ member =>
404
+ checker.getPropertiesOfType(member).length === 0 &&
405
+ (checker.getIndexInfosOfType?.(member) ?? []).length === 0,
406
+ );
407
+ if (
408
+ generic &&
409
+ (unnamed ||
410
+ members.some(member => namesFromTypeParameter(ts, checker, member, 0)))
411
+ ) {
412
+ return {
413
+ ok: false,
414
+ reason: `${exportName}'s props take some of their names from a type parameter, such as the element it renders, so which props it accepts is known only where it is used`,
415
+ };
416
+ }
417
+ // Each member of a prop's type is printed on its own, and long prints are
418
+ // truncated: a type that expands to a huge structure (StyleX styles, say)
419
+ // never matches a documented type anyway, and printing it whole can take
420
+ // gigabytes.
421
+ const flags =
422
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType |
423
+ ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
424
+
425
+ // `defaultProps` the package sets (a class's static field, one assigned to a
426
+ // function, or the wrapped component's through up to two layers of `memo`
427
+ // and `lazy`, as React's JSX types read it) lets JSX callers leave its props
428
+ // out; `any` fills every prop. React's own types declare an optional one on
429
+ // every component type.
430
+ let holder = type;
431
+ for (let i = 0; i < 2 && checker.getPropertyOfType(holder, '$$typeof'); i++) {
432
+ const inner =
433
+ checker.getPropertyOfType(holder, 'type') ??
434
+ checker.getPropertyOfType(holder, '_result');
435
+ if (!inner) break;
436
+ holder = checker.getTypeOfSymbolAtLocation(inner, sourceFile);
437
+ }
438
+ const defaultProps = checker.getPropertyOfType(holder, 'defaultProps');
439
+ const defaults = defaultProps?.declarations?.some(
440
+ /** @param {any} d */ d => isOwnFile(d.getSourceFile().fileName),
441
+ )
442
+ ? checker.getTypeOfSymbolAtLocation(defaultProps, sourceFile)
443
+ : null;
444
+ const fillsAll = !!defaults && !!(defaults.flags & ts.TypeFlags.Any);
445
+ const defaulted = new Set(
446
+ defaults
447
+ ? checker
448
+ .getPropertiesOfType(defaults)
449
+ .map(/** @param {any} s */ s => s.name)
450
+ : [],
451
+ );
452
+
453
+ /** @type {Map<string, {symbols: any[], optional: number}>} */
454
+ const byName = new Map();
455
+ for (const member of members) {
456
+ for (const prop of checker.getPropertiesOfType(member)) {
457
+ const entry = byName.get(prop.name) ?? {symbols: [], optional: 0};
458
+ entry.symbols.push(prop);
459
+ if (prop.flags & ts.SymbolFlags.Optional) entry.optional++;
460
+ byName.set(prop.name, entry);
461
+ }
462
+ }
463
+ if (fillsAll) for (const name of byName.keys()) defaulted.add(name);
464
+
465
+ /** @type {((prop: any) => string) | undefined} */
466
+ let origin;
467
+ // Read from the component's declaration, its props parameter's, and the
468
+ // wrapped component's for `memo` and `lazy`.
469
+ const originOf = (/** @type {any} */ prop) =>
470
+ (origin ??= mappedPropOrigin(ts, checker, isOwnFile, [
471
+ symbol.valueDeclaration ?? symbol.declarations?.[0],
472
+ ...[
473
+ ...signatures,
474
+ ...(holder === type ? [] : holder.getCallSignatures()),
475
+ ].map(
476
+ (/** @type {any} */ signature) =>
477
+ signature.getParameters()[0]?.valueDeclaration,
478
+ ),
479
+ ]))(prop);
480
+ for (const [name, {symbols, optional}] of byName) {
481
+ const declarations = symbols.flatMap(
482
+ /** @param {any} s */ s => s.declarations ?? [],
483
+ );
484
+ const ownDeclarations = declarations.filter(
485
+ /** @param {any} d */ d => isOwnFile(d.getSourceFile().fileName),
486
+ );
487
+ // A prop no declaration names, one a mapped type builds, is the
488
+ // package's own when its key comes from the package's own source.
489
+ const built =
490
+ declarations.length === 0 ? strongest(symbols.map(originOf)) : null;
491
+ // Optional when a caller may leave it out: an overload that takes no
492
+ // props, or a member of the props type that does not require it.
493
+ const required =
494
+ !defaulted.has(name) &&
495
+ !propless &&
496
+ symbols.length === members.length &&
497
+ optional === 0;
498
+ props.set(name, {
499
+ name,
500
+ required,
501
+ own: ownDeclarations.length > 0 || built === 'own',
502
+ untraced:
503
+ declarations.length === 0 && built !== 'own' && built !== 'foreign',
504
+ skipTagged: ownDeclarations.some(
505
+ /** @param {any} d */
506
+ d =>
507
+ ts
508
+ .getJSDocTags(d)
509
+ .some(
510
+ /** @param {any} tag */ tag => SKIP_TAGS.has(tag.tagName.text),
511
+ ),
512
+ ),
513
+ // A property's `type` is the prop's type; a method's is its return
514
+ // type, so a method prop reads as the compiler renders it. A
515
+ // declaration's text counts only for its own symbol, or where a mapping
516
+ // copies its type unchanged (`Pick`, `Omit`): one that rewrites it
517
+ // (`{[K in keyof T]: T[K][]}`, `Required<T>`) is judged by its type.
518
+ declared: symbols.flatMap(
519
+ /** @param {any} s */ s =>
520
+ (s.declarations ?? [])
521
+ .filter(
522
+ /** @param {any} d */ d =>
523
+ ownDeclarations.includes(d) &&
524
+ d.type &&
525
+ !ts.isMethodSignature(d) &&
526
+ !ts.isMethodDeclaration(d) &&
527
+ copiesType(ts, checker, s, d, sourceFile),
528
+ )
529
+ .map(/** @param {any} d */ d => d.type.getText(d.getSourceFile())),
530
+ ),
531
+ types: () =>
532
+ symbols.map(
533
+ /** @param {any} s */ s =>
534
+ checker.getTypeOfSymbolAtLocation(s, sourceFile),
535
+ ),
536
+ intersected: symbols.some(
537
+ /** @param {any} s */ s =>
538
+ (s.declarations ?? []).filter(/** @param {any} d */ d => d.type)
539
+ .length > 1,
540
+ ),
541
+ indexed: memo(() => {
542
+ const lacking = members.filter(
543
+ /** @param {any} m */ m => !checker.getPropertyOfType(m, name),
544
+ );
545
+ const each = lacking.map(
546
+ /** @param {any} m */ m => indexesFor(ts, checker, [m], name),
547
+ );
548
+ return each.some(indexes => indexes.length > 1)
549
+ ? null
550
+ : each.flatMap(indexes => indexes.map(info => info.type));
551
+ }),
552
+ location: ownDeclarations[0] ?? sourceFile,
553
+ // Printed where the prop is declared, where a documented type's names
554
+ // resolve: a type another file imports under another name prints as
555
+ // that file names it.
556
+ renderings: memo(() => {
557
+ const at = ownDeclarations[0] ?? sourceFile;
558
+ /** @type {string[]} */
559
+ const printed = [];
560
+ /** @type {string[]} */
561
+ const expanded = [];
562
+ for (const s of symbols) {
563
+ const propType = checker.getTypeOfSymbolAtLocation(s, sourceFile);
564
+ for (const part of inDeclaredOrder(ts, checker, propType, s)) {
565
+ printed.push(checker.typeToString(part, at, flags));
566
+ expanded.push(
567
+ checker.typeToString(
568
+ part,
569
+ at,
570
+ flags | ts.TypeFormatFlags.InTypeAlias,
571
+ ),
572
+ );
573
+ }
574
+ }
575
+ return [printed, expanded];
576
+ }),
577
+ });
578
+ }
579
+
580
+ /** The index signatures of the props type that take this name. */
581
+ const accepts = (/** @type {string} */ name) =>
582
+ indexesFor(ts, checker, members, name);
583
+ return {ok: true, props, accepts};
584
+ }
585
+
586
+ /**
587
+ * A prop type's union members in the order the source declares them,
588
+ * through the type aliases it names; members it doesn't reach, such as the
589
+ * `undefined` an optional prop adds, follow, sorted by their text. The
590
+ * compiler's own order follows when each type was first made, so it would
591
+ * move with whatever read the types first.
592
+ * @param {any} ts
593
+ * @param {any} checker
594
+ * @param {any} type
595
+ * @param {any} symbol the prop
596
+ * @returns {any[]}
597
+ */
598
+ function inDeclaredOrder(ts, checker, type, symbol) {
599
+ if (!type.isUnion()) return [type];
600
+ /** @type {any[]} */
601
+ const order = [];
602
+ const seen = new Set();
603
+ /** @type {(node: any) => void} */
604
+ const visit = node => {
605
+ if (ts.isParenthesizedTypeNode(node)) return visit(node.type);
606
+ if (ts.isUnionTypeNode(node)) return node.types.forEach(visit);
607
+ if (ts.isTypeReferenceNode(node) && !node.typeArguments) {
608
+ let target = checker.getSymbolAtLocation(node.typeName);
609
+ if (target && target.flags & ts.SymbolFlags.Alias) {
610
+ target = checker.getAliasedSymbol(target);
611
+ }
612
+ const alias = target?.declarations?.find(ts.isTypeAliasDeclaration);
613
+ if (alias && !alias.typeParameters) {
614
+ if (!seen.has(alias)) {
615
+ seen.add(alias);
616
+ visit(alias.type);
617
+ }
618
+ return;
619
+ }
620
+ }
621
+ const found = checker.getTypeFromTypeNode(node);
622
+ order.push(...(found.isUnion() ? found.types : [found]));
623
+ };
624
+ for (const declaration of symbol.declarations ?? []) {
625
+ if (declaration.type) visit(declaration.type);
626
+ }
627
+ /** @type {(t: any) => number} */
628
+ const place = t => {
629
+ const at = order.indexOf(t);
630
+ return at === -1 ? order.length : at;
631
+ };
632
+ /** @type {(t: any) => string} */
633
+ const text = t => checker.typeToString(t);
634
+ return [...type.types].sort(
635
+ (a, b) => place(a) - place(b) || text(a).localeCompare(text(b)),
636
+ );
637
+ }
638
+
639
+ /**
640
+ * Whether a declaration's text reads as the prop's type: it is the prop's own
641
+ * declaration, or the generic type's prop it instantiates when the result
642
+ * prints as the declared type in the component's file (a generic component
643
+ * passing its own type parameters on), or a mapping copies its type
644
+ * unchanged. Instantiated with another type (`Box<string>`, `ListProps<T[]>`,
645
+ * or an imported `Item` the file names another way), its text names a type
646
+ * the prop no longer has.
647
+ * @param {any} ts
648
+ * @param {any} checker
649
+ * @param {any} symbol the prop
650
+ * @param {any} declaration one of its declarations
651
+ * @param {any} location
652
+ */
653
+ function copiesType(ts, checker, symbol, declaration, location) {
654
+ const own = declaration.name && checker.getSymbolAtLocation(declaration.name);
655
+ if (!own) return false;
656
+ if (own === symbol) return true;
657
+ const type = checker.getTypeOfSymbolAtLocation(symbol, location);
658
+ const declared = checker.getTypeOfSymbolAtLocation(own, location);
659
+ /** @param {any} t */
660
+ const print = t =>
661
+ checker.typeToString(t, location, ts.TypeFormatFlags.NoTruncation);
662
+ return (
663
+ declared === type ||
664
+ (own === (symbol.links ?? symbol).target && print(type) === print(declared))
665
+ );
666
+ }
667
+
668
+ /**
669
+ * Does the code take `undefined` itself, written or through an alias? With
670
+ * `exactOptionalPropertyTypes` that is part of the type a message shows;
671
+ * otherwise an optional prop takes `undefined` anyway.
672
+ * @param {any} checker
673
+ * @param {CodeProp} code
674
+ * @param {boolean} exactOptional
675
+ */
676
+ function ownUndefined(checker, code, exactOptional) {
677
+ const undefinedType = checker.getUndefinedType?.();
678
+ return (
679
+ exactOptional &&
680
+ (code.declared.some(text => /\bundefined\b/.test(text)) ||
681
+ code
682
+ .types()
683
+ .some(t => (t.isUnion() ? t.types : [t]).includes(undefinedType)))
684
+ );
685
+ }
686
+
687
+ /**
688
+ * One of a prop's renderings for a message, without the `undefined` an
689
+ * optional prop adds, unless the code takes it itself.
690
+ * @param {CodeProp} code
691
+ * @param {number} which 0 as printed, 1 expanded
692
+ * @param {boolean} keepUndefined
693
+ */
694
+ function rendered(code, which, keepUndefined) {
695
+ const parts = code.renderings()[which];
696
+ return [
697
+ ...new Set(
698
+ parts.filter(
699
+ part =>
700
+ !(code.required !== true && part === 'undefined' && !keepUndefined),
701
+ ),
702
+ ),
703
+ ].join(' | ');
704
+ }
705
+
706
+ /**
707
+ * The prop's type for a message: as the source writes it when its
708
+ * declarations agree, else (a method or accessor, or declarations that differ
709
+ * or narrow each other in an intersection) as the compiler prints it.
710
+ * @param {CodeProp} code
711
+ * @param {boolean} keepUndefined
712
+ */
713
+ function codeTypeText(code, keepUndefined) {
714
+ const written = [...new Set(code.declared)];
715
+ const text =
716
+ written.length === 1 ? written[0] : rendered(code, 0, keepUndefined);
717
+ return text.replace(/\s+/g, ' ').trim();
718
+ }
719
+
720
+ /**
721
+ * Judge a documented type against a prop's code type (type-match.mjs).
722
+ * @param {any} ts
723
+ * @param {any} checker
724
+ * @param {string} docType
725
+ * @param {CodeProp} code
726
+ * @param {{nullable: boolean, exactOptional: boolean, isDefaultLibrary: (file: any) => boolean}} reading
727
+ * how the project checks null, and which files are TypeScript's own
728
+ */
729
+ function judgeProp(ts, checker, docType, code, reading) {
730
+ // A member that lacks the prop may take its name through an index
731
+ // signature, which a doc may describe too.
732
+ const indexed = code.indexed();
733
+ if (indexed == null) return 'not_compared';
734
+ return judgeMembers(docType, [...code.types(), ...indexed], {
735
+ ts,
736
+ checker,
737
+ location: code.location,
738
+ optional: code.required !== true && !reading.exactOptional,
739
+ ...reading,
740
+ // Declared text reads as the type only when each declaration is one
741
+ // member's; intersected declarations narrow each other.
742
+ written: code.intersected
743
+ ? code.renderings()
744
+ : [
745
+ code.declared,
746
+ ...code.declared.map(text => [text]),
747
+ ...code.renderings(),
748
+ ],
749
+ });
750
+ }
751
+
752
+ /**
753
+ * Judge a documented type against the types a prop has in the members of a
754
+ * union of props types: a doc may describe one member, so any member's type,
755
+ * or their union, matches, and it is a mismatch only against every one.
756
+ * @param {string} docType
757
+ * @param {any[]} types the type in each member
758
+ * @param {import('./type-match.mjs').JudgeContext} ctx
759
+ * @returns {import('./type-match.mjs').TypeVerdict}
760
+ */
761
+ function judgeMembers(docType, types, ctx) {
762
+ const candidates =
763
+ types.length > 1 ? [types, ...types.map(type => [type])] : [types];
764
+ const verdicts = candidates.map(candidate =>
765
+ judgeType(docType, candidate, ctx),
766
+ );
767
+ if (verdicts.includes('match')) return 'match';
768
+ return verdicts.every(verdict => verdict === 'mismatch')
769
+ ? 'mismatch'
770
+ : 'not_compared';
771
+ }
772
+
773
+ /**
774
+ * The props one doc entry names. `a / b` documents each of `a` and `b`, with
775
+ * the entry's `required` (and the type split the same way when it splits
776
+ * into as many parts); `x.y` (or
777
+ * `x[].y`) documents a field of prop `x`; `...rest` documents a spread of
778
+ * another props type and is not compared.
779
+ * @param {any} raw
780
+ * @returns {Array<{name: string, written: string, path: string[], spread: boolean, type?: string, required?: boolean}>}
781
+ */
782
+ function documentedProps(raw) {
783
+ const names = String(raw.name)
784
+ .split(/\s+\/\s+/)
785
+ .map(name => name.trim())
786
+ .filter(Boolean);
787
+ const types = typeof raw.type === 'string' ? raw.type.split(/\s+\/\s+/) : [];
788
+ return names.map((written, i) => {
789
+ const spread = written.startsWith('...');
790
+ const [root = written, ...path] = written
791
+ .replace(/\[\]/g, '')
792
+ .split('.')
793
+ .filter(Boolean);
794
+ return {
795
+ name: spread ? written : root,
796
+ written,
797
+ path: spread ? [] : path,
798
+ spread,
799
+ type:
800
+ names.length === 1
801
+ ? raw.type
802
+ : types.length === names.length
803
+ ? types[i]
804
+ : undefined,
805
+ required: raw.required,
806
+ };
807
+ });
808
+ }
809
+
810
+ /**
811
+ * The types a field can sit on: each member of a union, through arrays and
812
+ * tuples at any depth (`grid[][].id`).
813
+ * @param {any} ts
814
+ * @param {any} checker
815
+ * @param {any} type
816
+ * @param {number} [depth]
817
+ * @returns {any[]}
818
+ */
819
+ function holders(ts, checker, type, depth = 0) {
820
+ if (depth > 8) return [type];
821
+ if (type.isUnion()) {
822
+ return type.types.flatMap(
823
+ /** @param {any} t */ t => holders(ts, checker, t, depth + 1),
824
+ );
825
+ }
826
+ const array =
827
+ ['Array', 'ReadonlyArray'].includes(type.symbol?.name) ||
828
+ type.target?.objectFlags & ts.ObjectFlags.Tuple;
829
+ if (array && type.objectFlags & ts.ObjectFlags.Reference) {
830
+ return checker
831
+ .getTypeArguments(type)
832
+ .flatMap(/** @param {any} t */ t => holders(ts, checker, t, depth + 1));
833
+ }
834
+ return [type];
835
+ }
836
+
837
+ /**
838
+ * Whether a type takes `undefined` (an index signature's value type, which
839
+ * TypeScript holds an explicit `undefined` to).
840
+ * @param {any} ts
841
+ * @param {any} type
842
+ */
843
+ function takesUndefined(ts, type) {
844
+ return (type.isUnion() ? type.types : [type]).some(
845
+ /** @param {any} t */ t => t.flags & ts.TypeFlags.Undefined,
846
+ );
847
+ }
848
+
849
+ /**
850
+ * The types a field path reaches in a prop's type (in each member of a union
851
+ * that has it, through arrays at any depth), and whether its last field is
852
+ * optional: null when no member has the path, `any` when a type on the way
853
+ * is `any` or `unknown`, which holds any field.
854
+ * @param {any} ts
855
+ * @param {any} checker
856
+ * @param {CodeProp} code
857
+ * @param {string[]} fieldPath
858
+ * @returns {{types: any[], optional: boolean, ambiguous: boolean, indexed: boolean} | 'any' | null}
859
+ * `ambiguous` when two index signatures take a field on the path, and
860
+ * `indexed` when only index signatures take the last field
861
+ */
862
+ function fieldTypes(ts, checker, code, fieldPath) {
863
+ /** @type {any[]} */
864
+ let current = code.types();
865
+ let optional = false;
866
+ let ambiguous = false;
867
+ let indexed = false;
868
+ for (const field of fieldPath) {
869
+ /** @type {any[]} */
870
+ const next = [];
871
+ optional = false;
872
+ let byName = false;
873
+ for (const holder of current.flatMap(t => holders(ts, checker, t))) {
874
+ if (holder.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
875
+ return 'any';
876
+ }
877
+ const prop = checker.getPropertyOfType(holder, field);
878
+ if (prop) {
879
+ next.push(checker.getTypeOfSymbol(prop));
880
+ if (prop.flags & ts.SymbolFlags.Optional) optional = true;
881
+ byName = true;
882
+ continue;
883
+ }
884
+ // A field an index signature takes (`Record<string, string>`) is of the
885
+ // index's type, which takes `undefined` only when it says so; two that
886
+ // take it hold it to both.
887
+ const indexes = indexesFor(ts, checker, [holder], field);
888
+ if (indexes.length > 1) ambiguous = true;
889
+ if (indexes.length > 0) {
890
+ next.push(indexes[0].type);
891
+ if (takesUndefined(ts, indexes[0].type)) optional = true;
892
+ }
893
+ }
894
+ if (next.length === 0) return null;
895
+ current = next;
896
+ indexed = !byName;
897
+ }
898
+ return {types: current, optional, ambiguous, indexed};
899
+ }
900
+
901
+ /**
902
+ * The types a field path reaches, printed where the prop is declared, where
903
+ * a documented type's names resolve.
904
+ * @param {any} ts
905
+ * @param {any} checker
906
+ * @param {{types: any[], optional: boolean}} reached
907
+ * @param {boolean} exactOptional the project sets `exactOptionalPropertyTypes`
908
+ * @param {any} location where the prop is declared
909
+ * @returns {string[]}
910
+ */
911
+ function fieldTypeText(ts, checker, reached, exactOptional, location) {
912
+ // An optional field's `undefined` is the compiler's own; with
913
+ // `exactOptionalPropertyTypes` it is a marker of its own, and one the code
914
+ // declares stays.
915
+ const own = /** @param {any} t */ t =>
916
+ reached.optional &&
917
+ t.flags & ts.TypeFlags.Undefined &&
918
+ (!exactOptional || t !== checker.getUndefinedType?.());
919
+ return [
920
+ ...new Set(
921
+ reached.types
922
+ .flatMap(t => (t.isUnion() ? t.types : [t]))
923
+ .filter(t => !own(t))
924
+ .map(t =>
925
+ checker.typeToString(
926
+ t,
927
+ location,
928
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType,
929
+ ),
930
+ ),
931
+ ),
932
+ ];
933
+ }
934
+
935
+ /**
936
+ * Is a type the compiler's stand-in for a name it could not resolve?
937
+ * @param {any} ts
938
+ * @param {any} type
939
+ */
940
+ function isErrorType(ts, type) {
941
+ return (
942
+ (type.flags & ts.TypeFlags.Any) !== 0 && type.intrinsicName === 'error'
943
+ );
944
+ }
945
+
946
+ /**
947
+ * Do some of a generic component's prop names come from a type parameter:
948
+ * a deferred type, an index signature keyed or valued by a type made from
949
+ * one, or a mapped type whose keys are one (`Partial<P>`) or whose `as`
950
+ * clause uses one? An interface whose values alone use it (`ListProps<T>`),
951
+ * a mapping over keys of its own (`Record<'value', V>`), and an index that
952
+ * takes any name (`[key: string]: any`) give names of its own.
953
+ * @param {any} ts
954
+ * @param {any} checker
955
+ * @param {any} type
956
+ * @param {number} depth
957
+ * @returns {boolean}
958
+ */
959
+ function namesFromTypeParameter(ts, checker, type, depth) {
960
+ if (depth > 8) return false;
961
+ if (type.isUnion() || type.isIntersection()) {
962
+ return type.types.some(
963
+ /** @param {any} t */ t =>
964
+ namesFromTypeParameter(ts, checker, t, depth + 1),
965
+ );
966
+ }
967
+ if (isDeferred(ts, checker, type)) return true;
968
+ if (
969
+ (checker.getIndexInfosOfType?.(type) ?? []).some(
970
+ /** @param {any} info */ info =>
971
+ isDeferred(ts, checker, info.type, true) ||
972
+ isDeferred(ts, checker, info.keyType, true),
973
+ )
974
+ ) {
975
+ return true;
976
+ }
977
+ if ((type.objectFlags & ts.ObjectFlags.Mapped) === 0) return false;
978
+ // The keys, which the compiler works out when it reads the members.
979
+ checker.getPropertiesOfType(type);
980
+ return (
981
+ (!!type.constraintType && isDeferred(ts, checker, type.constraintType)) ||
982
+ renamedByTypeParameter(ts, checker, type)
983
+ );
984
+ }
985
+
986
+ /**
987
+ * Does a mapping's `as` clause use a type parameter besides its own key that
988
+ * is still open where the props are read: one the component declares, or one
989
+ * an alias passes on whose argument is still a type parameter (`` as
990
+ * `${N}${K}` ``, `as T extends true ? K : never`)? Given fixed arguments
991
+ * (`Prefixed<'my'>`, a key filter such as `Except<Props, 'b'>`), every key
992
+ * gets its name. Its own key, and an `infer` inside the clause, are known
993
+ * for each key.
994
+ * @param {any} ts
995
+ * @param {any} checker
996
+ * @param {any} type a mapped type
997
+ * @returns {boolean}
998
+ */
999
+ function renamedByTypeParameter(ts, checker, type) {
1000
+ const clause = type.declaration?.nameType;
1001
+ if (!clause) return false;
1002
+ /** @type {(node: any) => boolean} */
1003
+ const uses = node => {
1004
+ const symbol = ts.isTypeReferenceNode(node)
1005
+ ? checker.getSymbolAtLocation(node.typeName)
1006
+ : undefined;
1007
+ const declaration =
1008
+ symbol && symbol.flags & ts.SymbolFlags.TypeParameter
1009
+ ? symbol.declarations?.[0]
1010
+ : undefined;
1011
+ const inClause =
1012
+ declaration?.getSourceFile() === clause.getSourceFile() &&
1013
+ declaration.pos >= clause.pos &&
1014
+ declaration.end <= clause.end;
1015
+ if (
1016
+ declaration &&
1017
+ declaration !== type.declaration.typeParameter &&
1018
+ !inClause
1019
+ ) {
1020
+ // Its value where the props are read, through the instantiation.
1021
+ const value = valueIn(
1022
+ ts,
1023
+ checker,
1024
+ checker.getDeclaredTypeOfSymbol(symbol),
1025
+ type.mapper,
1026
+ 0,
1027
+ );
1028
+ if (!value || isDeferred(ts, checker, value, true)) return true;
1029
+ }
1030
+ return !!ts.forEachChild(
1031
+ node,
1032
+ /** @param {any} child */ child => uses(child) || undefined,
1033
+ );
1034
+ };
1035
+ return uses(clause);
1036
+ }
1037
+
1038
+ /**
1039
+ * The value an instantiation gives a type parameter, as the compiler's own
1040
+ * mappers give it, through each mapper one combines; null when that takes
1041
+ * more than following parameters (a value made from one, or a mapper of
1042
+ * another form), which counts as open.
1043
+ * @param {any} ts
1044
+ * @param {any} checker
1045
+ * @param {any} parameter
1046
+ * @param {any} mapper
1047
+ * @param {number} depth
1048
+ * @returns {any}
1049
+ */
1050
+ function valueIn(ts, checker, parameter, mapper, depth) {
1051
+ if (!mapper) return parameter;
1052
+ if (depth > 32) return null;
1053
+ if (mapper.mapper1 && mapper.mapper2) {
1054
+ const first = valueIn(ts, checker, parameter, mapper.mapper1, depth + 1);
1055
+ if (!first) return null;
1056
+ if (first === parameter || first.flags & ts.TypeFlags.TypeParameter) {
1057
+ return valueIn(ts, checker, first, mapper.mapper2, depth + 1);
1058
+ }
1059
+ return isDeferred(ts, checker, first, true) ? null : first;
1060
+ }
1061
+ if ('source' in mapper) {
1062
+ return mapper.source === parameter ? mapper.target : parameter;
1063
+ }
1064
+ if (Array.isArray(mapper.sources)) {
1065
+ const i = mapper.sources.indexOf(parameter);
1066
+ if (i < 0) return parameter;
1067
+ const target = mapper.targets?.[i];
1068
+ return typeof target === 'function' ? target() : (target ?? null);
1069
+ }
1070
+ return typeof mapper.func === 'function' ? mapper.func(parameter) : null;
1071
+ }
1072
+
1073
+ /**
1074
+ * A type parameter, or a type the compiler defers until one is known (an
1075
+ * indexed access, a conditional), or a union, intersection or template
1076
+ * literal with one (`on${K}`, `Capitalize<K>`); with `deep`, also a type made
1077
+ * from one (through alias or reference arguments).
1078
+ * @param {any} ts
1079
+ * @param {any} checker
1080
+ * @param {any} t
1081
+ * @param {boolean} [deep]
1082
+ * @param {number} [depth]
1083
+ * @returns {boolean}
1084
+ */
1085
+ function isDeferred(ts, checker, t, deep = false, depth = 0) {
1086
+ const F = ts.TypeFlags;
1087
+ if (
1088
+ t.flags &
1089
+ (F.TypeParameter |
1090
+ F.IndexedAccess |
1091
+ F.Conditional |
1092
+ F.Substitution |
1093
+ F.Index)
1094
+ ) {
1095
+ return true;
1096
+ }
1097
+ if (depth > 6) return false;
1098
+ const parts = [
1099
+ ...(t.isUnion() || t.isIntersection() ? t.types : []),
1100
+ ...(t.flags & F.TemplateLiteral ? (t.types ?? []) : []),
1101
+ ...(t.flags & F.StringMapping && t.type ? [t.type] : []),
1102
+ ...(deep ? (t.aliasTypeArguments ?? []) : []),
1103
+ ...(deep && t.objectFlags & ts.ObjectFlags.Reference
1104
+ ? checker.getTypeArguments(t)
1105
+ : []),
1106
+ ];
1107
+ return parts.some(u => isDeferred(ts, checker, u, deep, depth + 1));
1108
+ }
1109
+
1110
+ /**
1111
+ * Does an index signature's key take this prop name, as TypeScript would? A
1112
+ * `string` key takes any name; a template literal key only the names that
1113
+ * match it (`data-${string}`, `col${number}`, `on${Capitalize<string>}`).
1114
+ * @param {any} ts
1115
+ * @param {any} checker
1116
+ * @param {any} key
1117
+ * @param {string} name
1118
+ */
1119
+ function keyAccepts(ts, checker, key, name) {
1120
+ if (key.flags & ts.TypeFlags.String) return true;
1121
+ if (!(key.flags & ts.TypeFlags.TemplateLiteral)) return false;
1122
+ if (checker.getStringLiteralType && checker.isTypeAssignableTo) {
1123
+ return checker.isTypeAssignableTo(checker.getStringLiteralType(name), key);
1124
+ }
1125
+ // An older TypeScript has no literal-type factory: match the template.
1126
+ /** @type {Record<string, string>} */
1127
+ const mappings = {
1128
+ Uppercase: '[^a-z]*',
1129
+ Lowercase: '[^A-Z]*',
1130
+ Capitalize: '(?:[^a-z][\\s\\S]*)?',
1131
+ Uncapitalize: '(?:[^A-Z][\\s\\S]*)?',
1132
+ };
1133
+ const spans = (key.types ?? []).map(
1134
+ /** @param {any} span */ span =>
1135
+ span.flags & ts.TypeFlags.Number
1136
+ ? '[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?'
1137
+ : (mappings[String(span.symbol?.escapedName)] ?? '[\\s\\S]*'),
1138
+ );
1139
+ const pattern = (key.texts ?? [])
1140
+ .map(
1141
+ (/** @type {string} */ text, /** @type {number} */ i) =>
1142
+ text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + (spans[i] ?? ''),
1143
+ )
1144
+ .join('');
1145
+ return new RegExp(`^${pattern}$`).test(name);
1146
+ }
1147
+
1148
+ /**
1149
+ * The index signatures of these types that take a name, as TypeScript
1150
+ * matches keys. TypeScript holds a value to every one of them.
1151
+ * @param {any} ts
1152
+ * @param {any} checker
1153
+ * @param {any[]} types
1154
+ * @param {string} name
1155
+ * @returns {any[]}
1156
+ */
1157
+ function indexesFor(ts, checker, types, name) {
1158
+ // One signature seen through several union members is one signature.
1159
+ const seen = new Set();
1160
+ return types
1161
+ .flatMap(type => checker.getIndexInfosOfType?.(type) ?? [])
1162
+ .filter(
1163
+ /** @param {any} info */ info => {
1164
+ const key = `${checker.typeToString(info.keyType)}\0${checker.typeToString(info.type)}`;
1165
+ if (seen.has(key)) return false;
1166
+ seen.add(key);
1167
+ return (
1168
+ !holdsNothing(ts, info.type) &&
1169
+ keyAccepts(ts, checker, info.keyType, name)
1170
+ );
1171
+ },
1172
+ );
1173
+ }
1174
+
1175
+ /**
1176
+ * Can a value of this type be passed at all? `never`, or only `undefined`
1177
+ * (`Record<string, never>`, the usual way to write "no props"), cannot.
1178
+ * @param {any} ts
1179
+ * @param {any} type
1180
+ */
1181
+ function holdsNothing(ts, type) {
1182
+ const parts = type.isUnion() ? type.types : [type];
1183
+ return parts.every(
1184
+ /** @param {any} t */ t =>
1185
+ t.flags & (ts.TypeFlags.Never | ts.TypeFlags.Undefined),
1186
+ );
1187
+ }
1188
+
1189
+ /** @param {Array<string | null>} found the answers for one key, by place */
1190
+ const strongest = found =>
1191
+ ['own', 'foreign', 'unknown'].find(answer => found.includes(answer)) ?? null;
1192
+
1193
+ /**
1194
+ * Where the key of a prop a mapped type builds, which no declaration names,
1195
+ * comes from: `own` when the package's own source writes it, as a literal or
1196
+ * a property's declaration, `foreign` when another package's types do, and
1197
+ * `unknown` when the check can't trace it. It reads the mapping where the
1198
+ * component's declaration and props parameter reach it (and the component a
1199
+ * `memo`, `lazy` or `forwardRef` call wraps), through aliases, a generic's
1200
+ * arguments, `keyof`, `typeof`, and a conditional's branches (`Exclude`,
1201
+ * `Extract`).
1202
+ * @param {any} ts
1203
+ * @param {any} checker
1204
+ * @param {(file: string) => boolean} isOwnFile
1205
+ * @param {any[]} starts
1206
+ * @returns {(prop: any) => string}
1207
+ */
1208
+ function mappedPropOrigin(ts, checker, isOwnFile, starts) {
1209
+ /** @type {Map<any, Map<any, any>[]>} each mapping, with its type arguments */
1210
+ const sites = new Map();
1211
+ let budget = 5000;
1212
+ /** @param {any} node */
1213
+ const where = node =>
1214
+ isOwnFile(node.getSourceFile().fileName) ? 'own' : 'foreign';
1215
+ /** @param {any} node */
1216
+ const target = node => {
1217
+ let symbol = checker.getSymbolAtLocation(
1218
+ node.typeName ?? node.exprName ?? node.expression ?? node,
1219
+ );
1220
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
1221
+ symbol = checker.getAliasedSymbol(symbol);
1222
+ }
1223
+ return symbol;
1224
+ };
1225
+ /** @type {(params: any, args: any, env: Map<any, any>) => Map<any, any>} */
1226
+ const bind = (params, args, env) => {
1227
+ const next = new Map(env);
1228
+ (params ?? []).forEach(
1229
+ (/** @type {any} */ param, /** @type {number} */ i) => {
1230
+ const node = args?.[i] ?? param.default;
1231
+ if (node)
1232
+ next.set(target(param.name), {node, env: args?.[i] ? env : next});
1233
+ },
1234
+ );
1235
+ return next;
1236
+ };
1237
+ /** @type {(node: any, env: Map<any, any>, depth: number) => void} */
1238
+ const visit = (node, env, depth) => {
1239
+ if (!node || depth > 40 || budget-- <= 0) return;
1240
+ if (ts.isMappedTypeNode(node)) {
1241
+ sites.set(node, [...(sites.get(node) ?? []), env]);
1242
+ }
1243
+ const reference =
1244
+ ts.isTypeReferenceNode(node) || ts.isExpressionWithTypeArguments(node);
1245
+ // A component another binding wraps: `memo(Inner)`, `forwardRef(Impl)`.
1246
+ const wrapped =
1247
+ ts.isIdentifier(node) &&
1248
+ (ts.isCallExpression(node.parent) ||
1249
+ ts.isVariableDeclaration(node.parent)) &&
1250
+ node !== node.parent.expression &&
1251
+ node !== node.parent.name;
1252
+ for (const declaration of reference || wrapped
1253
+ ? (target(node)?.declarations ?? [])
1254
+ : []) {
1255
+ const own = where(declaration) === 'own';
1256
+ if (reference && ts.isTypeAliasDeclaration(declaration)) {
1257
+ visit(
1258
+ declaration.type,
1259
+ bind(declaration.typeParameters, node.typeArguments, env),
1260
+ depth + 1,
1261
+ );
1262
+ } else if (reference && (own || !node.typeArguments)) {
1263
+ // Another package's interface: only what it extends.
1264
+ const next = bind(declaration.typeParameters, node.typeArguments, env);
1265
+ if (own) visit(declaration, next, depth + 1);
1266
+ else
1267
+ for (const clause of declaration.heritageClauses ?? [])
1268
+ visit(clause, next, depth + 1);
1269
+ } else if (wrapped && own) {
1270
+ visit(declaration, env, depth + 1);
1271
+ }
1272
+ }
1273
+ ts.forEachChild(node, (/** @type {any} */ child) =>
1274
+ visit(child, env, depth + 1),
1275
+ );
1276
+ };
1277
+ /** @type {(node: any, key: string, env: Map<any, any>, depth: number) => string | null} */
1278
+ const keyOrigin = (node, key, env, depth) => {
1279
+ if (!node || depth > 40) return 'unknown';
1280
+ const next = (/** @type {any} */ inner) =>
1281
+ keyOrigin(inner, key, env, depth + 1);
1282
+ if (ts.isParenthesizedTypeNode(node)) return next(node.type);
1283
+ if (ts.isLiteralTypeNode(node)) {
1284
+ return node.literal.text === key ? where(node) : null;
1285
+ }
1286
+ if (ts.isUnionTypeNode(node)) return strongest(node.types.map(next));
1287
+ if (ts.isConditionalTypeNode(node)) {
1288
+ return strongest([next(node.trueType), next(node.falseType)]);
1289
+ }
1290
+ if (
1291
+ ts.isTypeOperatorNode(node) &&
1292
+ node.operator === ts.SyntaxKind.KeyOfKeyword
1293
+ ) {
1294
+ const of =
1295
+ ts.isTypeReferenceNode(node.type) && env.get(target(node.type));
1296
+ const prop = checker.getPropertyOfType(
1297
+ checker.getTypeFromTypeNode(of ? of.node : node.type),
1298
+ key,
1299
+ );
1300
+ return prop ? propOrigin(prop, depth + 1) : null;
1301
+ }
1302
+ if (ts.isArrayTypeNode(node)) return next(node.elementType);
1303
+ if (ts.isTupleTypeNode(node)) return strongest(node.elements.map(next));
1304
+ if (ts.isTypeOperatorNode(node)) return next(node.type);
1305
+ if (ts.isIndexedAccessTypeNode(node)) {
1306
+ // `Props['size']` reads the property's own type; `(typeof SIZES)[number]`
1307
+ // reads the value's.
1308
+ const index = ts.isLiteralTypeNode(node.indexType)
1309
+ ? node.indexType.literal.text
1310
+ : undefined;
1311
+ if (index === undefined) return next(node.objectType);
1312
+ const declared = checker.getPropertyOfType(
1313
+ checker.getTypeFromTypeNode(node.objectType),
1314
+ index,
1315
+ )?.declarations?.[0];
1316
+ return declared ? valueOrigin(declared, key, depth + 1) : 'unknown';
1317
+ }
1318
+ if (ts.isTypeQueryNode(node)) {
1319
+ return valueOrigin(target(node)?.valueDeclaration, key, depth + 1);
1320
+ }
1321
+ if (ts.isTypeReferenceNode(node)) {
1322
+ const symbol = target(node);
1323
+ const bound = symbol && env.get(symbol);
1324
+ if (bound) return keyOrigin(bound.node, key, bound.env, depth + 1);
1325
+ const alias = symbol?.declarations?.find(ts.isTypeAliasDeclaration);
1326
+ if (alias) {
1327
+ return keyOrigin(
1328
+ alias.type,
1329
+ key,
1330
+ bind(alias.typeParameters, node.typeArguments, env),
1331
+ depth + 1,
1332
+ );
1333
+ }
1334
+ // An enum: where it is declared. A type parameter no argument binds,
1335
+ // such as an `infer`, can't be traced.
1336
+ if (
1337
+ symbol &&
1338
+ symbol.flags & (ts.SymbolFlags.Enum | ts.SymbolFlags.EnumMember)
1339
+ ) {
1340
+ return where(symbol.declarations[0]);
1341
+ }
1342
+ }
1343
+ return 'unknown';
1344
+ };
1345
+ /**
1346
+ * Where a value's literal keys are written: its declared type, or the
1347
+ * literals its initializer writes, through `as const` and other bindings.
1348
+ * @type {(node: any, key: string, depth: number) => string | null}
1349
+ */
1350
+ const valueOrigin = (node, key, depth) => {
1351
+ if (!node || depth > 40) return 'unknown';
1352
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) {
1353
+ return node.text === key ? where(node) : null;
1354
+ }
1355
+ if (ts.isArrayLiteralExpression(node)) {
1356
+ return strongest(
1357
+ node.elements.map((/** @type {any} */ e) =>
1358
+ valueOrigin(e, key, depth + 1),
1359
+ ),
1360
+ );
1361
+ }
1362
+ // A declared type or a cast, but not `as const` or `satisfies`.
1363
+ const written =
1364
+ node.type &&
1365
+ !ts.isSatisfiesExpression?.(node) &&
1366
+ node.type.typeName?.text !== 'const';
1367
+ if (written) return keyOrigin(node.type, key, new Map(), depth + 1);
1368
+ const inner = node.initializer ?? node.expression;
1369
+ if (inner) return valueOrigin(inner, key, depth + 1);
1370
+ if (ts.isIdentifier(node)) {
1371
+ let symbol = checker.getSymbolAtLocation(node);
1372
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
1373
+ symbol = checker.getAliasedSymbol(symbol);
1374
+ }
1375
+ return valueOrigin(symbol?.valueDeclaration, key, depth + 1);
1376
+ }
1377
+ return 'unknown';
1378
+ };
1379
+ /** @type {(prop: any, depth: number) => string | null} */
1380
+ const propOrigin = (prop, depth) => {
1381
+ if (depth > 40) return 'unknown';
1382
+ const declarations = prop.declarations ?? [];
1383
+ if (declarations.length > 0) {
1384
+ return declarations.some((/** @type {any} */ d) => where(d) === 'own')
1385
+ ? 'own'
1386
+ : 'foreign';
1387
+ }
1388
+ const links = prop.links ?? prop;
1389
+ if (links.syntheticOrigin)
1390
+ return propOrigin(links.syntheticOrigin, depth + 1);
1391
+ if (links.containingType) {
1392
+ return strongest(
1393
+ links.containingType.types.map((/** @type {any} */ t) => {
1394
+ const part = checker.getPropertyOfType(t, prop.name);
1395
+ return part && part !== prop ? propOrigin(part, depth + 1) : null;
1396
+ }),
1397
+ );
1398
+ }
1399
+ const mapping = links.mappedType?.declaration;
1400
+ const key = links.keyType?.value;
1401
+ if (!mapping || key === undefined) return 'unknown';
1402
+ return strongest(
1403
+ (sites.get(mapping) ?? []).map(env =>
1404
+ keyOrigin(
1405
+ mapping.typeParameter.constraint,
1406
+ String(key),
1407
+ env,
1408
+ depth + 1,
1409
+ ),
1410
+ ),
1411
+ );
1412
+ };
1413
+ for (const start of starts) {
1414
+ if (start && where(start) === 'own') visit(start, new Map(), 0);
1415
+ }
1416
+ return prop => propOrigin(prop, 0) ?? 'unknown';
1417
+ }
1418
+
1419
+ /**
1420
+ * Are props `any`: a plain `any` (not one standing for a name that does not
1421
+ * resolve), or a type an alias makes from `any` by reading its keys, which
1422
+ * takes every name (`Omit<any, 'ref'>`, or React's `Omit` of one)? `Record<string, any>` only gives
1423
+ * names an `any` value, as an inline `[key: string]: any` does.
1424
+ * @param {any} ts
1425
+ * @param {any} checker
1426
+ * @param {any} type
1427
+ * @param {number} [depth] how far into an alias's arguments
1428
+ * @returns {boolean}
1429
+ */
1430
+ function isAnyProps(ts, checker, type, depth = 0) {
1431
+ if (type.flags & ts.TypeFlags.Any) return !isErrorType(ts, type);
1432
+ const parts = type.isUnion() || type.isIntersection() ? type.types : [type];
1433
+ /** @type {(node: any) => any} the symbol a reference names */
1434
+ const target = node => {
1435
+ const used = checker.getSymbolAtLocation(node.typeName ?? node.expression);
1436
+ return used && used.flags & ts.SymbolFlags.Alias
1437
+ ? checker.getAliasedSymbol(used)
1438
+ : used;
1439
+ };
1440
+ /** @type {(node: any) => any} */
1441
+ const aliasOf = node =>
1442
+ target(node)?.declarations?.find(ts.isTypeAliasDeclaration);
1443
+ // Whether an alias reads its `i`th parameter's keys: `keyof T`, or `T`
1444
+ // passed on to an alias that does, as `type Without<T> = Omit<T, 'as'>`.
1445
+ /** @type {(alias: any, i: number, depth?: number) => boolean} */
1446
+ const readsKeys = (alias, i, depth = 0) => {
1447
+ const name = alias?.typeParameters?.[i]?.name.text;
1448
+ /** @type {(node: any) => boolean} */
1449
+ const visit = node =>
1450
+ (ts.isTypeOperatorNode(node) &&
1451
+ node.operator === ts.SyntaxKind.KeyOfKeyword &&
1452
+ node.type.typeName?.text === name) ||
1453
+ (ts.isTypeReferenceNode(node) &&
1454
+ depth < 8 &&
1455
+ (node.typeArguments ?? []).some(
1456
+ (/** @type {any} */ arg, /** @type {number} */ j) =>
1457
+ arg.typeName?.text === name &&
1458
+ readsKeys(aliasOf(node), j, depth + 1),
1459
+ )) ||
1460
+ !!ts.forEachChild(
1461
+ node,
1462
+ (/** @type {any} */ child) => visit(child) || undefined,
1463
+ );
1464
+ return !!name && visit(alias);
1465
+ };
1466
+ /** @type {(symbol: any) => any[]} an alias's type, or an interface's bases */
1467
+ const shapeOf = symbol =>
1468
+ (symbol?.declarations ?? []).flatMap((/** @type {any} */ d) =>
1469
+ ts.isTypeAliasDeclaration(d)
1470
+ ? [d.type]
1471
+ : (d.heritageClauses?.flatMap((/** @type {any} */ c) => c.types) ?? []),
1472
+ );
1473
+ // `any`, or such a type, passed where an alias reads its keys, written in
1474
+ // the props type or one of the package's names for it (`type P =
1475
+ // Without<any>`, `interface P extends Omit<Omit<any, 'as'>, 'ref'>`).
1476
+ /** @type {(node: any, depth: number) => boolean} */
1477
+ const anyKeyed = (node, depth) => {
1478
+ if (!node || depth > 8) return false;
1479
+ if (node.kind === ts.SyntaxKind.AnyKeyword) return true;
1480
+ if (ts.isParenthesizedTypeNode(node)) return anyKeyed(node.type, depth + 1);
1481
+ if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
1482
+ return node.types.some((/** @type {any} */ t) => anyKeyed(t, depth + 1));
1483
+ }
1484
+ // A reference: `Name<…>`, or an interface's `extends Name<…>`.
1485
+ if (!node.typeName && !node.expression) return false;
1486
+ const alias = aliasOf(node);
1487
+ return node.typeArguments
1488
+ ? node.typeArguments.some(
1489
+ (/** @type {any} */ arg, /** @type {number} */ i) =>
1490
+ anyKeyed(arg, depth + 1) && readsKeys(alias, i),
1491
+ )
1492
+ : shapeOf(target(node)).some(n => anyKeyed(n, depth + 1));
1493
+ };
1494
+ return parts.some(
1495
+ /** @param {any} part */ part =>
1496
+ ((part.aliasTypeArguments ?? []).some(
1497
+ /** @type {(t: any, i: number) => boolean} */ (t, i) =>
1498
+ (t.flags & ts.TypeFlags.Any
1499
+ ? !isErrorType(ts, t)
1500
+ : depth < 4 && isAnyProps(ts, checker, t, depth + 1)) &&
1501
+ readsKeys(part.aliasSymbol?.declarations?.[0], i),
1502
+ ) ||
1503
+ shapeOf(part.aliasSymbol ?? part.symbol).some(n => anyKeyed(n, 0))) &&
1504
+ (checker.getIndexInfosOfType?.(part) ?? []).some(
1505
+ /** @param {any} info */ info => info.type.flags & ts.TypeFlags.Any,
1506
+ ),
1507
+ );
1508
+ }
1509
+
1510
+ /**
1511
+ * Is any part of a props type `any`, or built from a type the compiler could
1512
+ * not resolve (`Omit<Missing, 'x'>`, `Partial<Missing>`)? Then props it would
1513
+ * inherit are missing or unknown, and comparing would report documented
1514
+ * props as absent, or pass them unread.
1515
+ * @param {any} ts
1516
+ * @param {any} checker
1517
+ * @param {any} type
1518
+ * @param {number} depth
1519
+ * @param {Set<any>} seen types already read, so shared bases are read once
1520
+ * @returns {boolean}
1521
+ */
1522
+ function hasUnresolvedPart(ts, checker, type, depth, seen) {
1523
+ if (depth > 24 || seen.has(type)) return false;
1524
+ seen.add(type);
1525
+ if (type.flags & ts.TypeFlags.Any) return true;
1526
+ // A type made from another keeps what it was made from: an alias its
1527
+ // arguments, a generic reference its type arguments.
1528
+ const made = [
1529
+ ...(type.aliasTypeArguments ?? []),
1530
+ ...(type.objectFlags & ts.ObjectFlags.Reference
1531
+ ? checker.getTypeArguments(type)
1532
+ : []),
1533
+ ];
1534
+ if (made.some(/** @param {any} t */ t => isErrorType(ts, t))) return true;
1535
+ // A mapped type over one that does not resolve keeps no arguments behind a
1536
+ // named alias (`type Base = Pick<Missing, 'title'>`): its properties have
1537
+ // the compiler's error type.
1538
+ if (
1539
+ type.objectFlags & ts.ObjectFlags.Mapped &&
1540
+ checker
1541
+ .getPropertiesOfType(type)
1542
+ .some(
1543
+ /** @param {any} p */ p => isErrorType(ts, checker.getTypeOfSymbol(p)),
1544
+ )
1545
+ ) {
1546
+ return true;
1547
+ }
1548
+ // A mapped type written over one that does not resolve
1549
+ // (`{[K in keyof Missing]?: string}`) keeps it only in its declaration.
1550
+ const constraint = type.declaration?.typeParameter?.constraint;
1551
+ const mappedOver =
1552
+ constraint && ts.isTypeOperatorNode(constraint)
1553
+ ? constraint.type
1554
+ : constraint;
1555
+ if (
1556
+ type.objectFlags & ts.ObjectFlags.Mapped &&
1557
+ mappedOver &&
1558
+ isErrorType(ts, checker.getTypeFromTypeNode(mappedOver))
1559
+ ) {
1560
+ return true;
1561
+ }
1562
+ // A type mapped over one that does not resolve (`Omit<Missing, 'x'>`,
1563
+ // `Partial<Missing>`), reached directly, through a named alias, or nested,
1564
+ // reads as index signatures whose value is that unresolved type, which no
1565
+ // source declares.
1566
+ if (
1567
+ type.flags & ts.TypeFlags.Object &&
1568
+ (checker.getIndexInfosOfType?.(type) ?? []).some(
1569
+ /** @param {any} info */ info =>
1570
+ !info.declaration && isErrorType(ts, info.type),
1571
+ )
1572
+ ) {
1573
+ return true;
1574
+ }
1575
+ if (type.isUnion() || type.isIntersection()) {
1576
+ return type.types.some(
1577
+ /** @param {any} t */ t =>
1578
+ hasUnresolvedPart(ts, checker, t, depth + 1, seen),
1579
+ );
1580
+ }
1581
+ if (type.flags & ts.TypeFlags.Object) {
1582
+ const target =
1583
+ type.objectFlags & ts.ObjectFlags.Reference ? type.target : type;
1584
+ if (target.objectFlags & ts.ObjectFlags.ClassOrInterface) {
1585
+ // An unresolved `extends` is dropped from the base types, so read the
1586
+ // heritage clauses themselves.
1587
+ for (const declaration of target.symbol?.declarations ?? []) {
1588
+ for (const clause of declaration.heritageClauses ?? []) {
1589
+ for (const node of clause.types) {
1590
+ const base = checker.getTypeAtLocation(node);
1591
+ if (hasUnresolvedPart(ts, checker, base, depth + 1, seen))
1592
+ return true;
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+ return false;
1599
+ }
1600
+
1601
+ /**
1602
+ * Compare each documented prop table with the TypeScript props of the export
1603
+ * it describes.
1604
+ * @param {PropDriftTable[]} tables
1605
+ * @param {{packageDir: string, resolveFrom: Array<string | null | undefined>}} options
1606
+ * @returns {PropDriftReport}
1607
+ */
1608
+ export function comparePropTables(tables, options) {
1609
+ const {packageDir, resolveFrom} = options;
1610
+ const compiler = resolveProjectTypeScript(resolveFrom);
1611
+ if (!compiler?.ts) {
1612
+ return {
1613
+ status: 'skipped',
1614
+ reason: compiler
1615
+ ? unusableTypeScriptReason(
1616
+ compiler.version,
1617
+ 'documented props were not compared with the code',
1618
+ )
1619
+ : TYPESCRIPT_MISSING_REASON,
1620
+ components: 0,
1621
+ typesCompared: 0,
1622
+ findings: [],
1623
+ unchecked: [],
1624
+ typesNotCompared: [],
1625
+ };
1626
+ }
1627
+ const {ts, version} = compiler;
1628
+ let root = path.resolve(packageDir);
1629
+ try {
1630
+ root = fs.realpathSync(root);
1631
+ } catch {
1632
+ // Keep the resolved path.
1633
+ }
1634
+ const rel = (/** @type {string} */ file) =>
1635
+ path.relative(packageDir, file).split(path.sep).join('/');
1636
+
1637
+ /** @type {PropDriftUnchecked[]} */
1638
+ const unchecked = [];
1639
+ if (tables.length === 0) {
1640
+ return {
1641
+ status: 'checked',
1642
+ typescript: version,
1643
+ components: 0,
1644
+ typesCompared: 0,
1645
+ findings: [],
1646
+ unchecked,
1647
+ typesNotCompared: [],
1648
+ };
1649
+ }
1650
+
1651
+ const project = readPackageTsconfig(
1652
+ ts,
1653
+ packageDir,
1654
+ tables.flatMap(table => table.sources),
1655
+ );
1656
+ if (project.error) {
1657
+ // Read without the package's own config, the types would be guesses.
1658
+ for (const table of tables) {
1659
+ unchecked.push({
1660
+ component: table.name,
1661
+ doc: table.doc,
1662
+ reason: `the package's TypeScript config has an error, so props were not read (${project.error})`,
1663
+ });
1664
+ }
1665
+ unchecked.sort((a, b) => a.component.localeCompare(b.component));
1666
+ return {
1667
+ status: 'checked',
1668
+ typescript: version,
1669
+ components: 0,
1670
+ typesCompared: 0,
1671
+ findings: [],
1672
+ unchecked,
1673
+ typesNotCompared: [],
1674
+ };
1675
+ }
1676
+ const program = ts.createProgram({
1677
+ rootNames: [
1678
+ ...new Set([
1679
+ ...tables.flatMap(table => table.sources),
1680
+ ...project.declarations,
1681
+ ]),
1682
+ ],
1683
+ options: project.options,
1684
+ });
1685
+ const checker = program.getTypeChecker();
1686
+ const isOwnFile = ownFileTest(root);
1687
+ const ambient = unresolvedGlobals(
1688
+ ts,
1689
+ program,
1690
+ checker,
1691
+ project.declarations,
1692
+ project.options,
1693
+ isOwnFile,
1694
+ );
1695
+ // Without strict null checks the compiler drops `null` and `undefined`.
1696
+ // Whether they are on by default depends on the TypeScript version.
1697
+ const compilerOptions = program.getCompilerOptions();
1698
+ const nullable = !(typeof ts.getStrictOptionValue === 'function'
1699
+ ? ts.getStrictOptionValue(compilerOptions, 'strictNullChecks')
1700
+ : (compilerOptions.strictNullChecks ?? compilerOptions.strict ?? false));
1701
+ const reading = {
1702
+ nullable,
1703
+ // An optional prop takes `undefined` only when the project lets it.
1704
+ exactOptional:
1705
+ !nullable && compilerOptions.exactOptionalPropertyTypes === true,
1706
+ isDefaultLibrary: (/** @type {any} */ file) =>
1707
+ program.isSourceFileDefaultLibrary(file),
1708
+ };
1709
+
1710
+ /** @type {PropDriftFinding[]} */
1711
+ const findings = [];
1712
+ /** @type {Array<PropDriftUnchecked & {props: string[]}>} */
1713
+ const propsNotChecked = [];
1714
+ let compared = 0;
1715
+ let typesCompared = 0;
1716
+ /** @type {PropDriftTypesNotCompared[]} */
1717
+ const typesNotCompared = [];
1718
+ const typesSkipped = typeTextTooOld(ts);
1719
+ for (const table of tables) {
1720
+ /** @type {ReturnType<typeof readCodeProps> | null} */
1721
+ let read = null;
1722
+ let sourcePath = table.sources[0];
1723
+ for (const candidate of table.sources) {
1724
+ const attempt = readCodeProps(
1725
+ ts,
1726
+ program,
1727
+ checker,
1728
+ candidate,
1729
+ table.name,
1730
+ isOwnFile,
1731
+ ambient,
1732
+ );
1733
+ if (attempt.ok || read == null) {
1734
+ read = attempt;
1735
+ sourcePath = candidate;
1736
+ }
1737
+ if (attempt.ok) break;
1738
+ }
1739
+ if (read == null || !read.ok) {
1740
+ unchecked.push({
1741
+ component: table.name,
1742
+ doc: table.doc,
1743
+ reason:
1744
+ read != null && !read.ok
1745
+ ? read.reason
1746
+ : `no source file for ${table.name}`,
1747
+ });
1748
+ continue;
1749
+ }
1750
+ compared++;
1751
+ const source = rel(sourcePath);
1752
+ const documented = new Set();
1753
+ /** @type {Map<string, Set<string>>} */
1754
+ const notCompared = new Map();
1755
+ /** @type {(reason: string, name: string) => void} */
1756
+ const skip = (reason, name) => {
1757
+ notCompared.set(reason, (notCompared.get(reason) ?? new Set()).add(name));
1758
+ };
1759
+ for (const raw of table.props) {
1760
+ if (!raw || typeof raw.name !== 'string') continue;
1761
+ const entries = documentedProps(raw);
1762
+ for (const entry of entries) {
1763
+ const propName = entry.name;
1764
+ if (entry.spread) continue;
1765
+ const docType = entry.type;
1766
+ if (typesSkipped) entry.type = undefined;
1767
+ documented.add(propName);
1768
+ const code = read.props.get(propName);
1769
+ const reached =
1770
+ code && entry.path.length > 0
1771
+ ? fieldTypes(ts, checker, code, entry.path)
1772
+ : null;
1773
+ if (
1774
+ entry.type === undefined &&
1775
+ !typesSkipped &&
1776
+ typeof raw.type === 'string' &&
1777
+ (code && entry.path.length > 0
1778
+ ? reached
1779
+ : code || read.accepts(propName).length > 0)
1780
+ ) {
1781
+ // `a / b` with one type that does not split across the names (a
1782
+ // field only once it is found).
1783
+ skip(
1784
+ code && !code.own ? outside(propName, code) : NOT_EXACT,
1785
+ entry.written,
1786
+ );
1787
+ }
1788
+ const base = {
1789
+ component: table.name,
1790
+ prop: entry.written,
1791
+ severity: /** @type {const} */ ('warning'),
1792
+ doc: table.doc,
1793
+ source,
1794
+ };
1795
+ if (entry.path.length > 0) {
1796
+ // Listed only once the field is found; a missing one is reported.
1797
+ if (code && !code.own && reached && typeof entry.type === 'string') {
1798
+ skip(outside(propName, code), entry.written);
1799
+ }
1800
+ if (reached === 'any') {
1801
+ // A field of an `any` or `unknown` prop has no type to judge.
1802
+ if (code?.own && typeof entry.type === 'string') {
1803
+ skip(NOT_EXACT, entry.written);
1804
+ }
1805
+ continue;
1806
+ }
1807
+ if (code && reached) {
1808
+ const fieldText = fieldTypeText(
1809
+ ts,
1810
+ checker,
1811
+ reached,
1812
+ reading.exactOptional,
1813
+ code.location,
1814
+ );
1815
+ const ctx = {
1816
+ ts,
1817
+ checker,
1818
+ location: code.location,
1819
+ // A name an index signature takes sets the index's
1820
+ // `undefined` aside, as a prop of that name does.
1821
+ optional:
1822
+ reached.optional && (!reading.exactOptional || reached.indexed),
1823
+ ...reading,
1824
+ written: [fieldText],
1825
+ };
1826
+ const verdict =
1827
+ code.own && typeof entry.type === 'string'
1828
+ ? reached.ambiguous
1829
+ ? 'not_compared'
1830
+ : judgeType(entry.type, reached.types, ctx)
1831
+ : null;
1832
+ // The field takes every variant's type: the types of only some
1833
+ // variants are neither its type nor wrong. Past six, the sets of
1834
+ // them are not tried, and the type is never reported.
1835
+ const variants = [...new Set(reached.types)];
1836
+ const someVariants =
1837
+ (verdict === 'mismatch' || verdict === 'not_compared') &&
1838
+ !reached.ambiguous &&
1839
+ variants.length > 1 &&
1840
+ (variants.length > 6 ||
1841
+ Array.from({length: 2 ** variants.length - 2}, (_, i) =>
1842
+ variants.filter((__, j) => ((i + 1) >> j) & 1),
1843
+ ).some(
1844
+ some => judgeType(entry.type ?? '', some, ctx) === 'match',
1845
+ ));
1846
+ if (someVariants) skip(VARIANTS, entry.written);
1847
+ else if (verdict === 'not_compared') skip(NOT_EXACT, entry.written);
1848
+ else if (verdict) typesCompared++;
1849
+ if (verdict === 'mismatch' && !someVariants) {
1850
+ const codeType = fieldText.join(' | ');
1851
+ findings.push({
1852
+ ...base,
1853
+ kind: 'type_mismatch',
1854
+ message: `"${entry.written}" is documented as ${entry.type}, but the code declares ${codeType}.`,
1855
+ docType: entry.type,
1856
+ codeType,
1857
+ });
1858
+ }
1859
+ continue;
1860
+ }
1861
+ if (!code && read.accepts(propName).length > 0) {
1862
+ // A field of a name only an index signature takes.
1863
+ if (typeof entry.type === 'string') skip(NOT_EXACT, entry.written);
1864
+ continue;
1865
+ }
1866
+ findings.push({
1867
+ ...base,
1868
+ kind: 'not_in_code',
1869
+ message: code
1870
+ ? `"${entry.written}" is documented, but ${table.name}'s "${propName}" has no "${entry.path.join('.')}".`
1871
+ : `"${entry.written}" is documented, but ${table.name}'s props have no "${propName}".`,
1872
+ });
1873
+ continue;
1874
+ }
1875
+ if (!code) {
1876
+ // A name an index signature takes is a prop of the index's type;
1877
+ // TypeScript holds it to every signature that takes it, so with
1878
+ // several its type is not compared. Another package's signature is
1879
+ // not the package's to judge, and a mapped type's (`Record`) has no
1880
+ // declaration to say whose it is.
1881
+ const indexes = read.accepts(propName);
1882
+ const own = indexes.filter(
1883
+ (/** @type {any} */ i) =>
1884
+ i.declaration &&
1885
+ isOwnFile(i.declaration.getSourceFile().fileName),
1886
+ );
1887
+ const index = indexes.length === 1 ? own[0] : null;
1888
+ if (indexes.length > 0 && !index && typeof entry.type === 'string') {
1889
+ const declared = indexes.every(
1890
+ (/** @type {any} */ i) => i.declaration,
1891
+ );
1892
+ skip(
1893
+ own.length || !declared ? NOT_EXACT : outside(propName),
1894
+ propName,
1895
+ );
1896
+ }
1897
+ if (index && typeof entry.type === 'string') {
1898
+ const verdict = judgeType(entry.type, [index.type], {
1899
+ ts,
1900
+ checker,
1901
+ location: index.declaration,
1902
+ // An index signature takes `undefined` only when its type does.
1903
+ optional: takesUndefined(ts, index.type),
1904
+ ...reading,
1905
+ // Printed where the index is declared, as the doc's names
1906
+ // resolve there.
1907
+ written: [
1908
+ [
1909
+ checker.typeToString(
1910
+ index.type,
1911
+ index.declaration,
1912
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType,
1913
+ ),
1914
+ ],
1915
+ ],
1916
+ });
1917
+ if (verdict === 'not_compared') skip(NOT_EXACT, propName);
1918
+ else typesCompared++;
1919
+ if (verdict === 'mismatch') {
1920
+ const codeType = checker.typeToString(
1921
+ index.type,
1922
+ index.declaration,
1923
+ ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType,
1924
+ );
1925
+ findings.push({
1926
+ ...base,
1927
+ kind: 'type_mismatch',
1928
+ message: `"${propName}" is documented as ${entry.type}, but ${table.name}'s props take it as ${codeType}.`,
1929
+ docType: entry.type,
1930
+ codeType,
1931
+ });
1932
+ }
1933
+ }
1934
+ if (indexes.length > 0) continue;
1935
+ findings.push({
1936
+ ...base,
1937
+ kind: 'not_in_code',
1938
+ message: `"${propName}" is documented, but ${table.name}'s props have no "${propName}".`,
1939
+ ...(typeof docType === 'string' ? {docType} : {}),
1940
+ });
1941
+ continue;
1942
+ }
1943
+ const docRequired = entry.required === true;
1944
+ if (docRequired !== code.required) {
1945
+ findings.push({
1946
+ ...base,
1947
+ kind: 'required_mismatch',
1948
+ message: code.required
1949
+ ? `"${propName}" is required in the code, but the doc does not mark it required.`
1950
+ : `"${propName}" is documented as required, but it is optional in the code.`,
1951
+ docRequired,
1952
+ codeRequired: code.required,
1953
+ });
1954
+ }
1955
+ if (!code.own && typeof entry.type === 'string') {
1956
+ skip(outside(propName, code), propName);
1957
+ }
1958
+ if (code.own && typeof entry.type === 'string') {
1959
+ const verdict = judgeProp(
1960
+ ts,
1961
+ checker,
1962
+ String(entry.type),
1963
+ code,
1964
+ reading,
1965
+ );
1966
+ if (verdict === 'not_compared') skip(NOT_EXACT, propName);
1967
+ else typesCompared++;
1968
+ if (verdict === 'mismatch') {
1969
+ const keep = ownUndefined(checker, code, reading.exactOptional);
1970
+ const codeType = codeTypeText(code, keep);
1971
+ const expanded = rendered(code, 1, keep);
1972
+ // The optional prop's `undefined` the expansion sets aside is no
1973
+ // difference to show.
1974
+ const bare = codeType
1975
+ .split(' | ')
1976
+ .filter(part => part !== 'undefined')
1977
+ .join(' | ');
1978
+ const shown =
1979
+ expanded && expanded !== codeType && expanded !== bare
1980
+ ? `${codeType} (${expanded.length <= 400 ? expanded : `${expanded.slice(0, 400)}…`})`
1981
+ : codeType;
1982
+ findings.push({
1983
+ ...base,
1984
+ kind: 'type_mismatch',
1985
+ message: `"${propName}" is documented as ${entry.type}, but the code declares ${shown}.`,
1986
+ docType: entry.type,
1987
+ codeType,
1988
+ });
1989
+ }
1990
+ }
1991
+ }
1992
+ }
1993
+ /** @type {string[]} */
1994
+ const untraced = [];
1995
+ for (const code of read.props.values()) {
1996
+ if (
1997
+ documented.has(code.name) ||
1998
+ code.skipTagged ||
1999
+ PROPS_NOT_DOCUMENTED.includes(code.name)
2000
+ ) {
2001
+ continue;
2002
+ }
2003
+ if (!code.own) {
2004
+ if (code.untraced) untraced.push(code.name);
2005
+ continue;
2006
+ }
2007
+ const codeType = codeTypeText(
2008
+ code,
2009
+ ownUndefined(checker, code, reading.exactOptional),
2010
+ );
2011
+ findings.push({
2012
+ component: table.name,
2013
+ prop: code.name,
2014
+ kind: 'not_in_doc',
2015
+ severity: 'warning',
2016
+ message: `${table.name} declares "${code.name}", but the doc does not list it.`,
2017
+ doc: table.doc,
2018
+ source,
2019
+ codeType,
2020
+ codeRequired: code.required,
2021
+ });
2022
+ }
2023
+ if (untraced.length > 0) {
2024
+ propsNotChecked.push({
2025
+ component: table.name,
2026
+ doc: table.doc,
2027
+ reason: `a mapped type builds them from keys the check can't trace to the package's source, so it did not check that the doc lists them`,
2028
+ props: untraced.sort(),
2029
+ });
2030
+ }
2031
+ for (const [reason, props] of notCompared) {
2032
+ typesNotCompared.push({
2033
+ component: table.name,
2034
+ doc: table.doc,
2035
+ reason,
2036
+ props: [...props].sort(),
2037
+ });
2038
+ }
2039
+ }
2040
+
2041
+ // Field by field: collation ignores a separator such as NUL, which would
2042
+ // sort one component's findings among another's that shares its prefix.
2043
+ findings.sort(
2044
+ (a, b) =>
2045
+ a.component.localeCompare(b.component) ||
2046
+ a.prop.localeCompare(b.prop) ||
2047
+ a.kind.localeCompare(b.kind),
2048
+ );
2049
+ unchecked.sort((a, b) => a.component.localeCompare(b.component));
2050
+ typesNotCompared.sort(
2051
+ (a, b) =>
2052
+ a.component.localeCompare(b.component) ||
2053
+ a.reason.localeCompare(b.reason),
2054
+ );
2055
+ return {
2056
+ status: 'checked',
2057
+ typescript: version,
2058
+ components: compared,
2059
+ typesCompared,
2060
+ findings,
2061
+ unchecked,
2062
+ typesNotCompared,
2063
+ ...(typesSkipped ? {typesSkipped} : {}),
2064
+ ...(propsNotChecked.length > 0 ? {propsNotChecked} : {}),
2065
+ };
2066
+ }
2067
+
2068
+ /**
2069
+ * The prop tables one doc file documents, each with the export it describes.
2070
+ * A multi-component doc names each entry's export; an entry without `props`
2071
+ * (a reference to a sibling doc) and a hook entry are not prop tables.
2072
+ * @param {any} doc
2073
+ * @param {PropDriftComponent} component
2074
+ * @returns {Array<{name: string, props: any[]}>}
2075
+ */
2076
+ function propTables(doc, component) {
2077
+ if (!doc || typeof doc !== 'object') return [];
2078
+ /** @type {Array<{name: string, props: any[]}>} */
2079
+ const tables = [];
2080
+ // A doc's own props document the export its `name` names, which can be
2081
+ // another file's export than the one its file is named after.
2082
+ const own = ownName(doc, component);
2083
+ if (Array.isArray(doc.props) && !HOOK_NAME.test(own)) {
2084
+ tables.push({name: own, props: doc.props});
2085
+ }
2086
+ if (Array.isArray(doc.components)) {
2087
+ for (const entry of doc.components) {
2088
+ if (
2089
+ entry &&
2090
+ typeof entry.name === 'string' &&
2091
+ Array.isArray(entry.props) &&
2092
+ !(Array.isArray(doc.props) && entry.name === own) &&
2093
+ !HOOK_NAME.test(entry.name)
2094
+ ) {
2095
+ tables.push({name: entry.name, props: entry.props});
2096
+ }
2097
+ }
2098
+ }
2099
+ return tables;
2100
+ }
2101
+
2102
+ /**
2103
+ * The export a doc's own props document: its `name`, else its record's.
2104
+ * @param {any} doc
2105
+ * @param {PropDriftComponent} component
2106
+ */
2107
+ function ownName(doc, component) {
2108
+ return typeof doc.name === 'string' && doc.name !== ''
2109
+ ? doc.name
2110
+ : component.name;
2111
+ }
2112
+
2113
+ /**
2114
+ * The stem of a `<stem>.doc.{ts,mjs,js}` file: the name of the record that
2115
+ * owns the doc.
2116
+ * @param {string} docPath
2117
+ */
2118
+ function docStem(docPath) {
2119
+ return path.basename(docPath).replace(/\.doc\.(ts|mjs|js)$/, '');
2120
+ }
2121
+
2122
+ /**
2123
+ * Where an entry's export lives: `<entry>.tsx` beside the doc, else the doc's
2124
+ * own same-stem source, else the directory's `index.ts`/`index.tsx`, else the
2125
+ * `.tsx` in the doc's folder that declares it, as discovery finds a member's.
2126
+ * @param {string} name
2127
+ * @param {PropDriftComponent} component
2128
+ * @returns {string[]} candidates, in order
2129
+ */
2130
+ function sourcesForEntry(name, component) {
2131
+ const own = component.sourcePath ? [component.sourcePath] : [];
2132
+ if (name === component.name) return own;
2133
+ const dir = path.dirname(component.docPath);
2134
+ return [
2135
+ path.join(dir, `${name}.tsx`),
2136
+ ...own,
2137
+ path.join(dir, 'index.ts'),
2138
+ path.join(dir, 'index.tsx'),
2139
+ folderSourceFor(dir, name),
2140
+ ].filter(
2141
+ /** @type {(file: string | null, i: number, all: Array<string | null>) => file is string} */ (
2142
+ (file, i, all) =>
2143
+ file != null && all.indexOf(file) === i && fs.existsSync(file)
2144
+ ),
2145
+ );
2146
+ }
2147
+
2148
+ /**
2149
+ * Compare documented props with each component's TypeScript props: find each
2150
+ * doc's prop tables, then compare them (`comparePropTables`).
2151
+ * @param {PropDriftComponent[]} components
2152
+ * @param {{packageDir: string, resolveFrom: Array<string | null | undefined>, loadDoc?: (docPath: string) => Promise<any>}} options
2153
+ * @returns {Promise<PropDriftReport>}
2154
+ */
2155
+ export async function comparePropDocs(components, options) {
2156
+ const {packageDir, resolveFrom, loadDoc = loadComponentDoc} = options;
2157
+ const rel = (/** @type {string} */ file) =>
2158
+ path.relative(packageDir, file).split(path.sep).join('/');
2159
+
2160
+ /** @type {PropDriftTable[]} */
2161
+ const tables = [];
2162
+ /** @type {PropDriftUnchecked[]} */
2163
+ const unchecked = [];
2164
+ const seen = new Set();
2165
+ // A family doc can stand behind several records: its own, named after the
2166
+ // doc, and one per member entry that discovery records as a component of
2167
+ // its own. A member record compares only its own entry, against its own
2168
+ // source; the family record leaves those entries to the members.
2169
+ /** @type {Map<string, Set<string>>} */
2170
+ const namesByDoc = new Map();
2171
+ for (const component of components) {
2172
+ const names = namesByDoc.get(component.docPath) ?? new Set();
2173
+ names.add(component.name);
2174
+ namesByDoc.set(component.docPath, names);
2175
+ }
2176
+ /** @type {Map<string, Promise<any>>} */
2177
+ const docs = new Map();
2178
+ /** @type {Set<string>} docs whose own record is here */
2179
+ const owned = new Set();
2180
+ // Its own props, and every entry no member record compares: a hidden one,
2181
+ // or one discovery doesn't record. One whose source isn't found is listed
2182
+ // as not compared.
2183
+ const compareOwn = (
2184
+ /** @type {PropDriftComponent} */ component,
2185
+ /** @type {any} */ doc,
2186
+ ) => {
2187
+ const members = namesByDoc.get(component.docPath) ?? new Set();
2188
+ for (const table of propTables(doc, component)) {
2189
+ if (table.name !== component.name && members.has(table.name)) continue;
2190
+ const sources = sourcesForEntry(table.name, component);
2191
+ // One table per doc: two docs that each document a component both
2192
+ // get compared.
2193
+ const key = `${component.docPath}\0${sources[0]}\0${table.name}`;
2194
+ if (seen.has(key)) continue;
2195
+ seen.add(key);
2196
+ tables.push({doc: rel(component.docPath), ...table, sources});
2197
+ }
2198
+ };
2199
+ for (const component of components) {
2200
+ let doc;
2201
+ try {
2202
+ let loading = docs.get(component.docPath);
2203
+ if (!loading) {
2204
+ loading = Promise.resolve(loadDoc(component.docPath));
2205
+ docs.set(component.docPath, loading);
2206
+ }
2207
+ doc = await loading;
2208
+ } catch (err) {
2209
+ unchecked.push({
2210
+ component: component.name,
2211
+ doc: rel(component.docPath),
2212
+ reason: `its doc does not load: ${err instanceof Error ? err.message : String(err)}`,
2213
+ });
2214
+ continue;
2215
+ }
2216
+ // The doc's own record is named after its file, or after the export its
2217
+ // `name` names; any other record is a family member's.
2218
+ const ownRecord =
2219
+ component.name === docStem(component.docPath) ||
2220
+ (doc != null &&
2221
+ typeof doc === 'object' &&
2222
+ ownName(doc, component) === component.name);
2223
+ if (!ownRecord) {
2224
+ // A family member compares its own entry.
2225
+ const entry = Array.isArray(doc?.components)
2226
+ ? doc.components.find(
2227
+ (/** @type {any} */ candidate) =>
2228
+ candidate?.name === component.name &&
2229
+ Array.isArray(candidate.props),
2230
+ )
2231
+ : undefined;
2232
+ if (!entry || HOOK_NAME.test(component.name)) continue;
2233
+ const key = `${component.docPath}\0${component.sourcePath}\0${component.name}`;
2234
+ if (seen.has(key)) continue;
2235
+ seen.add(key);
2236
+ tables.push({
2237
+ doc: rel(component.docPath),
2238
+ name: component.name,
2239
+ props: entry.props,
2240
+ sources: component.sourcePath ? [component.sourcePath] : [],
2241
+ });
2242
+ continue;
2243
+ }
2244
+ owned.add(component.docPath);
2245
+ compareOwn(component, doc);
2246
+ }
2247
+ // A family doc whose own record is missing, because its same-stem source
2248
+ // is, still has its own props and unrecorded entries compared or listed.
2249
+ for (const [docPath, loading] of docs) {
2250
+ if (owned.has(docPath)) continue;
2251
+ const doc = await loading.catch(() => null);
2252
+ if (doc == null || typeof doc !== 'object') continue;
2253
+ compareOwn({name: docStem(docPath), docPath, sourcePath: null}, doc);
2254
+ }
2255
+ const report = comparePropTables(tables, {packageDir, resolveFrom});
2256
+ if (report.status === 'skipped') return report;
2257
+ return {
2258
+ ...report,
2259
+ unchecked: [...unchecked, ...report.unchecked].sort((a, b) =>
2260
+ a.component.localeCompare(b.component),
2261
+ ),
2262
+ };
2263
+ }