@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.e26723d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/component.test.mjs +420 -167
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/component/list/list.mjs +42 -4
- package/api/docs/_adapter.mjs +207 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +7 -3
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/integrationComponentConflicts.doc.mjs +21 -5
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +108 -16
- package/api/search/search.test.mjs +36 -0
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/template/template.test.mjs +120 -3
- package/api/theme/build/build.family.test.mjs +16 -0
- package/api/theme/build/build.mjs +81 -22
- package/api/theme/themeBuild.doc.mjs +3 -3
- package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
- package/assets/docs/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +10 -0
- package/assets/docs/tree/check-the-props.doc.mjs +145 -0
- package/assets/docs/tree/checks.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/links.doc.mjs +2 -2
- package/assets/docs/tree/single-component.doc.mjs +3 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/versioning.doc.mjs +1 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +43 -4
- package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
- package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
- package/assets/templates/pages/detail-page/page.tsx +3 -3
- package/assets/templates/pages/table-inbox/page.tsx +10 -8
- package/assets/theme.template.ts +1 -1
- package/authoring/doctypes/base/type.ts +6 -0
- package/authoring/doctypes/component/component.doc.mjs +4 -4
- package/authoring/doctypes/component/type.ts +6 -3
- package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +8 -1
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/doctor-integration-components.doc.mjs +9 -3
- package/clients/cli/commands/doctor-integration.test.mjs +392 -0
- package/clients/cli/commands/doctor.mjs +74 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/component-discovery.d.mts +190 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +864 -56
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1224 -0
- package/foundation/discovery/doc-types.test.mjs +652 -0
- package/foundation/discovery/docs-discovery.d.mts +81 -0
- package/foundation/discovery/docs-discovery.mjs +57 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +220 -0
- package/foundation/discovery/prop-drift.mjs +1765 -0
- package/foundation/discovery/prop-drift.test.mjs +1197 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +9 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/doc-compiler/links.d.mts +9 -5
- package/foundation/doc-compiler/links.mjs +4 -2
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Judge a documented prop type against the type the code declares,
|
|
5
|
+
* only where both sides can be judged exactly.
|
|
6
|
+
*
|
|
7
|
+
* A documented type matches when its text equals the type as the source
|
|
8
|
+
* writes it (or as the compiler prints it) once type-text.mjs normalizes
|
|
9
|
+
* both, unless a `React.X` on either side names another type than a bare `X`
|
|
10
|
+
* does where the prop is declared (`React.KeyboardEvent` beside the DOM's
|
|
11
|
+
* `KeyboardEvent`, or a file's own `Key`); text that writes `readonly` matches only exactly as written,
|
|
12
|
+
* whitespace aside. Otherwise both sides are judged only when each is built from exact
|
|
13
|
+
* parts: the primitives `string`, `number`, `boolean`, `bigint`, `symbol`,
|
|
14
|
+
* `null`, `undefined`, `void` and `object`; string, number and boolean
|
|
15
|
+
* literals; string enum members (each of its own enum: another enum's member
|
|
16
|
+
* of the same value is another type, and a member beside the literal of its
|
|
17
|
+
* own value adds nothing, as TypeScript passes a member where that literal is
|
|
18
|
+
* taken); arrays of these (`T[]`, `Array<T>`,
|
|
19
|
+
* `ReadonlyArray<T>`); unions of these; and a name that resolves to one of
|
|
20
|
+
* these where the prop is declared (not one only TypeScript's own libraries
|
|
21
|
+
* declare, which the code may not mean; `React.X` resolves through `React`
|
|
22
|
+
* there, as the code would read it). The two sides must then hold the same
|
|
23
|
+
* parts, in any order, as TypeScript reduces a union at any depth (a literal
|
|
24
|
+
* beside its primitive is absorbed by it), with `undefined` set aside on an
|
|
25
|
+
* optional prop, and `null` and `undefined` dropped at any depth when the
|
|
26
|
+
* project has no strict null checks.
|
|
27
|
+
*
|
|
28
|
+
* Every other form is not compared, and never reported as a mismatch: a
|
|
29
|
+
* callback, an object, a tuple, a generic with arguments, `keyof`, an
|
|
30
|
+
* intersection, a mapped or conditional type, `any` or `unknown`, a numeric
|
|
31
|
+
* enum, two enums of one name (TypeScript relates them by their members, by
|
|
32
|
+
* rules its versions differ on), and a type that does not resolve.
|
|
33
|
+
*
|
|
34
|
+
* @input A documented type string, the prop's types, and the compiler.
|
|
35
|
+
* @output 'match', 'mismatch' or 'not_compared'.
|
|
36
|
+
* @position Used by the prop-drift comparison (prop-drift.mjs).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import {normalizeTypeText, UNPARSED} from './type-text.mjs';
|
|
40
|
+
|
|
41
|
+
/** @typedef {'match' | 'mismatch' | 'not_compared'} TypeVerdict */
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} JudgeContext
|
|
45
|
+
* @property {any} ts
|
|
46
|
+
* @property {any} checker
|
|
47
|
+
* @property {any} location where names in the documented type resolve (the
|
|
48
|
+
* prop's declaration, else the component's source file)
|
|
49
|
+
* @property {boolean} optional the prop is optional, so `undefined` is set
|
|
50
|
+
* aside on both sides
|
|
51
|
+
* @property {(file: any) => boolean} [isDefaultLibrary] whether a source
|
|
52
|
+
* file is one of TypeScript's own libraries
|
|
53
|
+
* @property {boolean} [nullable] the project checks without strict null
|
|
54
|
+
* checks, so the compiler drops `null` and `undefined` from every type and
|
|
55
|
+
* both are set aside on both sides
|
|
56
|
+
* @property {boolean} [exactOptional] the project sets
|
|
57
|
+
* `exactOptionalPropertyTypes`: an optional prop takes `undefined` only when
|
|
58
|
+
* its type says so, and the compiler's mark for an absent prop is not a
|
|
59
|
+
* type the prop takes
|
|
60
|
+
* @property {string[][]} written the code's type as the source writes it and
|
|
61
|
+
* as the compiler prints it, each a list of union members
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
const MAX_DEPTH = 6;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A string literal as one part: quoted, with quotes and backslashes escaped,
|
|
68
|
+
* so no text inside it reads as other parts (`"a'|'b"` is one literal).
|
|
69
|
+
* @param {string} value
|
|
70
|
+
*/
|
|
71
|
+
const literalPart = value =>
|
|
72
|
+
`'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Text as part of an enum part: each character but a word character as its
|
|
76
|
+
* code point (`%41;`), so no text reads as a separator and no string, an
|
|
77
|
+
* unpaired surrogate included, fails to encode.
|
|
78
|
+
* @param {string} text
|
|
79
|
+
*/
|
|
80
|
+
const encodePart = text =>
|
|
81
|
+
[...text]
|
|
82
|
+
.map(c =>
|
|
83
|
+
/[\w-]/.test(c) ? c : `%${(c.codePointAt(0) ?? 0).toString(16)};`,
|
|
84
|
+
)
|
|
85
|
+
.join('');
|
|
86
|
+
|
|
87
|
+
/** @param {string} text */
|
|
88
|
+
const decodePart = text =>
|
|
89
|
+
text.replace(/%([0-9a-f]+);/g, (_, hex) =>
|
|
90
|
+
String.fromCodePoint(parseInt(hex, 16)),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
/** Type keywords, which never name an alias. */
|
|
94
|
+
const KEYWORD_TYPES = new Set([
|
|
95
|
+
'string',
|
|
96
|
+
'number',
|
|
97
|
+
'boolean',
|
|
98
|
+
'bigint',
|
|
99
|
+
'symbol',
|
|
100
|
+
'object',
|
|
101
|
+
'any',
|
|
102
|
+
'unknown',
|
|
103
|
+
'never',
|
|
104
|
+
'void',
|
|
105
|
+
'undefined',
|
|
106
|
+
'null',
|
|
107
|
+
'true',
|
|
108
|
+
'false',
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Judge a documented type. A doc may name an alias and spell out what it
|
|
113
|
+
* stands for, as `Name = expansion` or `Name (expansion)` (text that is not a
|
|
114
|
+
* type by itself): both parts are judged, and a mismatch in either is one;
|
|
115
|
+
* with one value after it (`Size = 'md'`, `Size = Size.Md`), or after a
|
|
116
|
+
* keyword, that part may be a default, and only the name is judged.
|
|
117
|
+
* @param {string} docType
|
|
118
|
+
* @param {any[]} codeTypes the prop's type in each member of the props type
|
|
119
|
+
* @param {JudgeContext} ctx
|
|
120
|
+
* @returns {TypeVerdict}
|
|
121
|
+
*/
|
|
122
|
+
export function judgeType(docType, codeTypes, ctx) {
|
|
123
|
+
const notation = parseType(ctx.ts, docType)
|
|
124
|
+
? null
|
|
125
|
+
: (/^\s*([A-Za-z_$][\w$.]*)\s*=(?!>)\s*([\s\S]+)$/.exec(docType) ??
|
|
126
|
+
/^\s*([A-Za-z_$][\w$.]*)\s*\(([\s\S]+)\)\s*$/.exec(docType));
|
|
127
|
+
/** @type {string[]} */
|
|
128
|
+
let texts = [docType];
|
|
129
|
+
if (notation) {
|
|
130
|
+
const [, name, rest] = notation;
|
|
131
|
+
const restNode = parseType(ctx.ts, rest);
|
|
132
|
+
if (!restNode) return 'not_compared'; // prose, not a type or a default
|
|
133
|
+
// After a type keyword, or as one value (`'md'`, `Size.Md`), the second
|
|
134
|
+
// part may be the prop's default, not an alias's expansion: only the
|
|
135
|
+
// type is judged.
|
|
136
|
+
const isDefault =
|
|
137
|
+
KEYWORD_TYPES.has(name) ||
|
|
138
|
+
(restNode != null && isValue(ctx.ts, restNode));
|
|
139
|
+
texts = isDefault ? [name] : [name, rest];
|
|
140
|
+
}
|
|
141
|
+
const verdicts = texts.map(text => judgeOne(text, codeTypes, ctx));
|
|
142
|
+
if (verdicts.includes('mismatch')) return 'mismatch';
|
|
143
|
+
return verdicts.every(verdict => verdict === 'match')
|
|
144
|
+
? 'match'
|
|
145
|
+
: 'not_compared';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A string literal, kept as written, or a run of whitespace. */
|
|
149
|
+
const QUOTED_OR_SPACE =
|
|
150
|
+
/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`|\s+/g;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A type's text without whitespace outside string literals, but for one space
|
|
154
|
+
* between two words.
|
|
155
|
+
* @param {string} text
|
|
156
|
+
*/
|
|
157
|
+
function asWritten(text) {
|
|
158
|
+
return text
|
|
159
|
+
.trim()
|
|
160
|
+
.replace(QUOTED_OR_SPACE, (match, offset, whole) =>
|
|
161
|
+
!/^\s/.test(match)
|
|
162
|
+
? match
|
|
163
|
+
: /[\w$]/.test(whole[offset - 1] ?? '') &&
|
|
164
|
+
/[\w$]/.test(whole[offset + match.length] ?? '')
|
|
165
|
+
? ' '
|
|
166
|
+
: '',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string} docType
|
|
172
|
+
* @param {any[]} codeTypes
|
|
173
|
+
* @param {JudgeContext} ctx
|
|
174
|
+
* @returns {TypeVerdict}
|
|
175
|
+
*/
|
|
176
|
+
function judgeOne(docType, codeTypes, ctx) {
|
|
177
|
+
const {optional} = ctx;
|
|
178
|
+
const doc = normalizeTypeText(docType, {optional, ts: ctx.ts}).join('|');
|
|
179
|
+
// Text that does not parse is never a match.
|
|
180
|
+
if (doc.startsWith(UNPARSED)) return 'not_compared';
|
|
181
|
+
// The text match sets `readonly` aside, so text that writes `readonly`
|
|
182
|
+
// matches only as written: the same text, whitespace aside, with
|
|
183
|
+
// `readonly` kept on both sides. Any other goes to the parts below.
|
|
184
|
+
// Outside string literals: `'readonly'` is a word, not the modifier.
|
|
185
|
+
const readonly = (/** @type {string} */ text) =>
|
|
186
|
+
/\breadonly\b(?!\s*[:?])|\bReadonly(?:Array)?\s*</.test(
|
|
187
|
+
text.replace(QUOTED_OR_SPACE, part => (/^\s/.test(part) ? part : "''")),
|
|
188
|
+
);
|
|
189
|
+
const docReadonly = readonly(docType);
|
|
190
|
+
const sameText = ctx.written.some(
|
|
191
|
+
texts =>
|
|
192
|
+
(docReadonly || texts.some(readonly)
|
|
193
|
+
? texts.length === 1 && asWritten(texts[0]) === asWritten(docType)
|
|
194
|
+
: texts.length > 0 &&
|
|
195
|
+
[
|
|
196
|
+
...new Set(
|
|
197
|
+
texts.flatMap(text =>
|
|
198
|
+
normalizeTypeText(text, {optional, ts: ctx.ts}),
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
]
|
|
202
|
+
.sort()
|
|
203
|
+
.join('|') === doc) && !reactNameDiffers(ctx, docType, texts),
|
|
204
|
+
);
|
|
205
|
+
if (sameText) return 'match';
|
|
206
|
+
const node = parseType(ctx.ts, docType);
|
|
207
|
+
const docParts = node ? docAtoms(ctx, node, 0) : null;
|
|
208
|
+
const codeParts = codeAtoms(ctx, codeTypes, 0);
|
|
209
|
+
if (!docParts || !codeParts) return 'not_compared';
|
|
210
|
+
// `readonly` set aside, outside string literals.
|
|
211
|
+
const mutable = (/** @type {string[]} */ atoms) =>
|
|
212
|
+
reduced(
|
|
213
|
+
ctx,
|
|
214
|
+
atoms.map(atom =>
|
|
215
|
+
atom.replace(/'(?:\\.|[^'\\])*'|readonly\[/g, part =>
|
|
216
|
+
part === 'readonly[' ? '[' : part,
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
true,
|
|
220
|
+
);
|
|
221
|
+
const doc_ = reduced(ctx, docParts, true);
|
|
222
|
+
const code = reduced(ctx, codeParts, true);
|
|
223
|
+
// A doc that names a mutable array where the code takes a readonly one
|
|
224
|
+
// matches, as TypeScript passes it.
|
|
225
|
+
if (doc_ === code || doc_ === mutable(codeParts)) return 'match';
|
|
226
|
+
// Two enums of one name are not judged (see the file's comment).
|
|
227
|
+
const owners = new Set(
|
|
228
|
+
[...docParts, ...codeParts].join('|').match(/enum:[^#|\]]*#[^#|\]]*/g),
|
|
229
|
+
);
|
|
230
|
+
const names = [...owners].map(owner => owner.split('#')[0]);
|
|
231
|
+
if (new Set(names).size < names.length) return 'not_compared';
|
|
232
|
+
if (mutable(docParts) !== mutable(codeParts)) return 'mismatch';
|
|
233
|
+
// Only `readonly` differs: a readonly doc for mutable code is wrong; a
|
|
234
|
+
// nested mix is not placed.
|
|
235
|
+
return code === mutable(codeParts) ? 'mismatch' : 'not_compared';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A union's exact parts, as TypeScript reduces them, at any depth: a literal
|
|
240
|
+
* beside its primitive is absorbed by it (`IconName | string` is `string`),
|
|
241
|
+
* and `null` and `undefined` are dropped without strict null checks, as the
|
|
242
|
+
* compiler drops them. On an optional prop `undefined` is set aside too, as
|
|
243
|
+
* the prop takes it anyway.
|
|
244
|
+
* @param {JudgeContext} ctx
|
|
245
|
+
* @param {string[]} atoms
|
|
246
|
+
* @param {boolean} top the prop's own type, not an array element
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
function reduced(ctx, atoms, top) {
|
|
250
|
+
const kept = new Set(
|
|
251
|
+
atoms.filter(
|
|
252
|
+
atom =>
|
|
253
|
+
!(atom === 'undefined' && ((top && ctx.optional) || ctx.nullable)) &&
|
|
254
|
+
!(atom === 'null' && ctx.nullable),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
if (kept.has('string')) {
|
|
258
|
+
for (const atom of [...kept]) {
|
|
259
|
+
if (atom.startsWith("'") || atom.startsWith('enum:')) kept.delete(atom);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const atom of [...kept]) {
|
|
263
|
+
const value = atom.startsWith('enum:')
|
|
264
|
+
? decodePart(atom.slice(atom.lastIndexOf('#') + 1))
|
|
265
|
+
: null;
|
|
266
|
+
if (value != null && kept.has(literalPart(value))) kept.delete(atom);
|
|
267
|
+
}
|
|
268
|
+
if (kept.has('number')) {
|
|
269
|
+
for (const atom of [...kept]) {
|
|
270
|
+
if (/^-?\d/.test(atom) || /^(?:NaN|-?Infinity)$/.test(atom)) {
|
|
271
|
+
kept.delete(atom);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// A member another covers adds nothing, as TypeScript passes it there:
|
|
276
|
+
// `undefined` beside `void`, an array beside `object`, and an array whose
|
|
277
|
+
// element types another array (readonly, or as mutable) also takes.
|
|
278
|
+
if (kept.has('void')) kept.delete('undefined');
|
|
279
|
+
for (const atom of [...kept]) {
|
|
280
|
+
if (
|
|
281
|
+
isArrayPart(atom) &&
|
|
282
|
+
(kept.has('object') ||
|
|
283
|
+
[...kept].some(other => other !== atom && arrayCovers(other, atom)))
|
|
284
|
+
) {
|
|
285
|
+
kept.delete(atom);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return [...kept].sort().join('|');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** @param {string} atom */
|
|
292
|
+
const isArrayPart = atom => /^(?:readonly)?\[/.test(atom);
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The parts of a reduced union's text, split at the top level, outside
|
|
296
|
+
* string literals and arrays.
|
|
297
|
+
* @param {string} text
|
|
298
|
+
* @returns {string[]}
|
|
299
|
+
*/
|
|
300
|
+
function unionParts(text) {
|
|
301
|
+
/** @type {string[]} */
|
|
302
|
+
const parts = [];
|
|
303
|
+
let depth = 0;
|
|
304
|
+
let quoted = false;
|
|
305
|
+
let start = 0;
|
|
306
|
+
for (let i = 0; i < text.length; i++) {
|
|
307
|
+
const c = text[i];
|
|
308
|
+
if (quoted) {
|
|
309
|
+
if (c === '\\') i++;
|
|
310
|
+
else if (c === "'") quoted = false;
|
|
311
|
+
} else if (c === "'") quoted = true;
|
|
312
|
+
else if (c === '[') depth++;
|
|
313
|
+
else if (c === ']') depth--;
|
|
314
|
+
else if (c === '|' && depth === 0) {
|
|
315
|
+
parts.push(text.slice(start, i));
|
|
316
|
+
start = i + 1;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
parts.push(text.slice(start));
|
|
320
|
+
return parts.filter(Boolean);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Does array part `wide` take every value of array part `narrow`: readonly,
|
|
325
|
+
* or both mutable, with each of `narrow`'s element types one of `wide`'s or
|
|
326
|
+
* covered by one?
|
|
327
|
+
* @param {string} wide
|
|
328
|
+
* @param {string} narrow
|
|
329
|
+
* @returns {boolean}
|
|
330
|
+
*/
|
|
331
|
+
function arrayCovers(wide, narrow) {
|
|
332
|
+
if (!isArrayPart(wide) || !isArrayPart(narrow)) return false;
|
|
333
|
+
const readonly = (/** @type {string} */ part) => part.startsWith('readonly');
|
|
334
|
+
if (readonly(narrow) && !readonly(wide)) return false;
|
|
335
|
+
const inner = (/** @type {string} */ part) =>
|
|
336
|
+
unionParts(part.slice(part.indexOf('[') + 1, -1));
|
|
337
|
+
const taken = inner(wide);
|
|
338
|
+
return inner(narrow).every(
|
|
339
|
+
part =>
|
|
340
|
+
taken.includes(part) ||
|
|
341
|
+
(taken.includes('string') &&
|
|
342
|
+
(part.startsWith("'") || part.startsWith('enum:'))) ||
|
|
343
|
+
(taken.includes('number') && /^(?:-?\d|NaN$|-?Infinity$)/.test(part)) ||
|
|
344
|
+
(part === 'undefined' && taken.includes('void')) ||
|
|
345
|
+
(isArrayPart(part) &&
|
|
346
|
+
(taken.includes('object') ||
|
|
347
|
+
taken.some(other => arrayCovers(other, part)))),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Is a type node one value a default could be: a literal (`'md'`, `0`,
|
|
353
|
+
* `false`, `null`), or a name with no type arguments (`Size.Md`)?
|
|
354
|
+
* @param {any} ts
|
|
355
|
+
* @param {any} node
|
|
356
|
+
* @returns {boolean}
|
|
357
|
+
*/
|
|
358
|
+
function isValue(ts, node) {
|
|
359
|
+
if (ts.isParenthesizedTypeNode(node)) return isValue(ts, node.type);
|
|
360
|
+
return (
|
|
361
|
+
ts.isLiteralTypeNode(node) ||
|
|
362
|
+
node.kind === ts.SyntaxKind.UndefinedKeyword ||
|
|
363
|
+
node.kind === ts.SyntaxKind.NullKeyword ||
|
|
364
|
+
(ts.isTypeReferenceNode(node) && !node.typeArguments?.length)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Parse a type expression with the project's TypeScript.
|
|
370
|
+
* @param {any} ts
|
|
371
|
+
* @param {string} text
|
|
372
|
+
* @returns {any | null} the type node, or null when the text is not a type
|
|
373
|
+
*/
|
|
374
|
+
function parseType(ts, text) {
|
|
375
|
+
const source = ts.createSourceFile(
|
|
376
|
+
'__documented_type__.ts',
|
|
377
|
+
`type __Documented = ${text};`,
|
|
378
|
+
ts.ScriptTarget.Latest,
|
|
379
|
+
true,
|
|
380
|
+
ts.ScriptKind.TS,
|
|
381
|
+
);
|
|
382
|
+
const [statement, ...rest] = source.statements;
|
|
383
|
+
if (
|
|
384
|
+
(source.parseDiagnostics?.length ?? 0) > 0 ||
|
|
385
|
+
rest.length > 0 ||
|
|
386
|
+
!statement ||
|
|
387
|
+
!ts.isTypeAliasDeclaration(statement) ||
|
|
388
|
+
statement.type.end !== source.text.length - 1
|
|
389
|
+
) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
return statement.type;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The exact parts of a documented type, or null when it has a part that is
|
|
397
|
+
* not exact.
|
|
398
|
+
* @param {JudgeContext} ctx
|
|
399
|
+
* @param {any} node
|
|
400
|
+
* @param {number} depth
|
|
401
|
+
* @returns {string[] | null}
|
|
402
|
+
*/
|
|
403
|
+
function docAtoms(ctx, node, depth) {
|
|
404
|
+
const {ts} = ctx;
|
|
405
|
+
const SyntaxKind = ts.SyntaxKind;
|
|
406
|
+
if (depth > MAX_DEPTH) return null;
|
|
407
|
+
if (ts.isParenthesizedTypeNode(node)) return docAtoms(ctx, node.type, depth);
|
|
408
|
+
if (ts.isUnionTypeNode(node)) {
|
|
409
|
+
const parts = node.types.map(
|
|
410
|
+
/** @param {any} t */ t => docAtoms(ctx, t, depth + 1),
|
|
411
|
+
);
|
|
412
|
+
return parts.every(Boolean) ? parts.flat() : null;
|
|
413
|
+
}
|
|
414
|
+
/** @type {Record<number, string[]>} */
|
|
415
|
+
const keywords = {
|
|
416
|
+
[SyntaxKind.StringKeyword]: ['string'],
|
|
417
|
+
[SyntaxKind.NumberKeyword]: ['number'],
|
|
418
|
+
[SyntaxKind.BooleanKeyword]: ['false', 'true'],
|
|
419
|
+
[SyntaxKind.BigIntKeyword]: ['bigint'],
|
|
420
|
+
[SyntaxKind.SymbolKeyword]: ['symbol'],
|
|
421
|
+
[SyntaxKind.UndefinedKeyword]: ['undefined'],
|
|
422
|
+
[SyntaxKind.VoidKeyword]: ['void'],
|
|
423
|
+
[SyntaxKind.ObjectKeyword]: ['object'],
|
|
424
|
+
[SyntaxKind.NullKeyword]: ['null'],
|
|
425
|
+
[SyntaxKind.NeverKeyword]: [],
|
|
426
|
+
};
|
|
427
|
+
if (keywords[node.kind]) return keywords[node.kind];
|
|
428
|
+
if (ts.isLiteralTypeNode(node)) {
|
|
429
|
+
const literal = node.literal;
|
|
430
|
+
if (
|
|
431
|
+
ts.isStringLiteral(literal) ||
|
|
432
|
+
ts.isNoSubstitutionTemplateLiteral(literal)
|
|
433
|
+
)
|
|
434
|
+
return [literalPart(literal.text)];
|
|
435
|
+
if (ts.isNumericLiteral(literal)) return [String(Number(literal.text))];
|
|
436
|
+
if (
|
|
437
|
+
ts.isPrefixUnaryExpression(literal) &&
|
|
438
|
+
literal.operator === SyntaxKind.MinusToken &&
|
|
439
|
+
ts.isNumericLiteral(literal.operand)
|
|
440
|
+
) {
|
|
441
|
+
return [String(-Number(literal.operand.text))];
|
|
442
|
+
}
|
|
443
|
+
if (literal.kind === SyntaxKind.TrueKeyword) return ['true'];
|
|
444
|
+
if (literal.kind === SyntaxKind.FalseKeyword) return ['false'];
|
|
445
|
+
if (literal.kind === SyntaxKind.NullKeyword) return ['null'];
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
// A readonly array is its own type: TypeScript does not pass one where a
|
|
449
|
+
// mutable array is taken.
|
|
450
|
+
const array = (/** @type {any} */ element, readonly = false) => {
|
|
451
|
+
const parts = docAtoms(ctx, element, depth + 1);
|
|
452
|
+
return parts
|
|
453
|
+
? [`${readonly ? 'readonly' : ''}[${reduced(ctx, parts, false)}]`]
|
|
454
|
+
: null;
|
|
455
|
+
};
|
|
456
|
+
if (ts.isArrayTypeNode(node)) return array(node.elementType);
|
|
457
|
+
if (
|
|
458
|
+
ts.isTypeOperatorNode(node) &&
|
|
459
|
+
node.operator === SyntaxKind.ReadonlyKeyword &&
|
|
460
|
+
ts.isArrayTypeNode(node.type)
|
|
461
|
+
) {
|
|
462
|
+
return array(node.type.elementType, true);
|
|
463
|
+
}
|
|
464
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
465
|
+
const name = referenceName(ts, node.typeName);
|
|
466
|
+
const args = node.typeArguments ?? [];
|
|
467
|
+
if ((name === 'Array' || name === 'ReadonlyArray') && args.length === 1)
|
|
468
|
+
return array(args[0], name === 'ReadonlyArray');
|
|
469
|
+
if (args.length > 0) return null;
|
|
470
|
+
const named = resolveName(ctx, name);
|
|
471
|
+
return named ? codeAtoms(ctx, [named], depth + 1) : null;
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The exact parts of the code's type, or null when it has a part that is not
|
|
478
|
+
* exact.
|
|
479
|
+
* @param {JudgeContext} ctx
|
|
480
|
+
* @param {any[]} types
|
|
481
|
+
* @param {number} depth
|
|
482
|
+
* @returns {string[] | null}
|
|
483
|
+
*/
|
|
484
|
+
function codeAtoms(ctx, types, depth) {
|
|
485
|
+
const {ts, checker} = ctx;
|
|
486
|
+
const F = ts.TypeFlags;
|
|
487
|
+
if (depth > MAX_DEPTH) return null;
|
|
488
|
+
/** @type {string[]} */
|
|
489
|
+
const out = [];
|
|
490
|
+
for (const type of types) {
|
|
491
|
+
for (const t of type.isUnion() ? type.types : [type]) {
|
|
492
|
+
if (t.flags & F.EnumLiteral) {
|
|
493
|
+
// A string enum member is exact, and only its own enum's: TypeScript
|
|
494
|
+
// takes no other enum's member of the same value. A numeric enum also
|
|
495
|
+
// takes numbers.
|
|
496
|
+
const decl = t.symbol?.valueDeclaration;
|
|
497
|
+
const owner = decl && checker.getSymbolAtLocation(decl.parent.name);
|
|
498
|
+
if (!(t.flags & F.StringLiteral) || !owner) return null;
|
|
499
|
+
out.push(
|
|
500
|
+
`enum:${owner.name}#${encodePart(checker.getFullyQualifiedName(owner))}#${encodePart(t.value)}`,
|
|
501
|
+
);
|
|
502
|
+
} else if (t.flags & F.StringLiteral) out.push(literalPart(t.value));
|
|
503
|
+
else if (t.flags & F.NumberLiteral) out.push(String(t.value));
|
|
504
|
+
else if (t.flags & F.BooleanLiteral) out.push(t.intrinsicName);
|
|
505
|
+
else if (t.flags & F.String) out.push('string');
|
|
506
|
+
else if (t.flags & F.Number) out.push('number');
|
|
507
|
+
else if (t.flags & F.BigInt) out.push('bigint');
|
|
508
|
+
else if (t.flags & F.ESSymbol) out.push('symbol');
|
|
509
|
+
else if (t.flags & F.Null) out.push('null');
|
|
510
|
+
else if (t.flags & F.Undefined) {
|
|
511
|
+
// `exactOptionalPropertyTypes` marks an optional prop with its own
|
|
512
|
+
// `undefined`, which `prop={undefined}` does not satisfy.
|
|
513
|
+
if (!ctx.exactOptional || t === ctx.checker.getUndefinedType?.()) {
|
|
514
|
+
out.push('undefined');
|
|
515
|
+
}
|
|
516
|
+
} else if (t.flags & F.Void) out.push('void');
|
|
517
|
+
else if (t.flags & F.NonPrimitive) out.push('object');
|
|
518
|
+
else if (t.flags & F.Never) continue;
|
|
519
|
+
else {
|
|
520
|
+
const name = t.symbol?.name;
|
|
521
|
+
const element =
|
|
522
|
+
(name === 'Array' || name === 'ReadonlyArray') &&
|
|
523
|
+
t.objectFlags & ts.ObjectFlags.Reference
|
|
524
|
+
? checker.getTypeArguments(t)[0]
|
|
525
|
+
: undefined;
|
|
526
|
+
const parts = element ? codeAtoms(ctx, [element], depth + 1) : null;
|
|
527
|
+
if (!parts) return null;
|
|
528
|
+
out.push(
|
|
529
|
+
`${name === 'ReadonlyArray' ? 'readonly' : ''}[${reduced(ctx, parts, false)}]`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The text of a type reference's name, `React.` kept: it resolves through
|
|
539
|
+
* `React` where the prop is declared, which may not be what a bare name
|
|
540
|
+
* means there.
|
|
541
|
+
* @param {any} ts
|
|
542
|
+
* @param {any} typeName
|
|
543
|
+
* @returns {string}
|
|
544
|
+
*/
|
|
545
|
+
function referenceName(ts, typeName) {
|
|
546
|
+
if (ts.isIdentifier(typeName)) return typeName.text;
|
|
547
|
+
return `${referenceName(ts, typeName.left)}.${typeName.right.text}`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Does one side write `React.X` where the other writes a bare `X` that names
|
|
552
|
+
* another type where the prop is declared (the DOM's `KeyboardEvent`, a
|
|
553
|
+
* file's own `Key`)? The text reading sets the qualifier aside, so then the
|
|
554
|
+
* two must not be read as one. A bare name that resolves to nothing there
|
|
555
|
+
* leaves `React.X` as what it means.
|
|
556
|
+
* @param {JudgeContext} ctx
|
|
557
|
+
* @param {string} docType
|
|
558
|
+
* @param {string[]} texts the code's type, one way it is written
|
|
559
|
+
* @returns {boolean}
|
|
560
|
+
*/
|
|
561
|
+
function reactNameDiffers(ctx, docType, texts) {
|
|
562
|
+
const names = (/** @type {string[]} */ list) => {
|
|
563
|
+
const masked = list
|
|
564
|
+
.map(text =>
|
|
565
|
+
text.replace(QUOTED_OR_SPACE, part => (/^\s/.test(part) ? ' ' : "''")),
|
|
566
|
+
)
|
|
567
|
+
.join(' ');
|
|
568
|
+
const qualified = new Set(
|
|
569
|
+
[...masked.matchAll(/(?<![\w$.])React\s*\.\s*([A-Za-z_$][\w$]*)/g)].map(
|
|
570
|
+
match => match[1],
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
const bare = new Set(
|
|
574
|
+
[...masked.matchAll(/(?<![\w$.]|\.\s)([A-Za-z_$][\w$]*)/g)].map(
|
|
575
|
+
match => match[1],
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
return {qualified, bare};
|
|
579
|
+
};
|
|
580
|
+
const doc = names([docType]);
|
|
581
|
+
const code = names(texts);
|
|
582
|
+
const crossed = [
|
|
583
|
+
...[...doc.qualified].filter(name => code.bare.has(name)),
|
|
584
|
+
...[...code.qualified].filter(name => doc.bare.has(name)),
|
|
585
|
+
];
|
|
586
|
+
return crossed.some(name => {
|
|
587
|
+
const bare = resolveSymbol(ctx, name);
|
|
588
|
+
return !!bare && bare !== resolveSymbol(ctx, `React.${name}`);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* The type a name the documented type uses resolves to, where the prop is
|
|
594
|
+
* declared, or null.
|
|
595
|
+
* @param {JudgeContext} ctx
|
|
596
|
+
* @param {string} name
|
|
597
|
+
* @returns {any | null}
|
|
598
|
+
*/
|
|
599
|
+
function resolveName(ctx, name) {
|
|
600
|
+
const {ts, checker} = ctx;
|
|
601
|
+
try {
|
|
602
|
+
const resolved = resolveSymbol(ctx, name);
|
|
603
|
+
// A name only TypeScript's own libraries declare (the DOM's
|
|
604
|
+
// `ScrollBehavior`) is one the code may not mean: not resolved.
|
|
605
|
+
const declarations = resolved?.declarations ?? [];
|
|
606
|
+
if (
|
|
607
|
+
ctx.isDefaultLibrary &&
|
|
608
|
+
declarations.length > 0 &&
|
|
609
|
+
declarations.every(
|
|
610
|
+
/** @param {any} d */ d => ctx.isDefaultLibrary?.(d.getSourceFile()),
|
|
611
|
+
)
|
|
612
|
+
) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return resolved && resolved.flags & ts.SymbolFlags.Type
|
|
616
|
+
? checker.getDeclaredTypeOfSymbol(resolved)
|
|
617
|
+
: null;
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* The symbol a name resolves to where the prop is declared, through aliases
|
|
625
|
+
* and namespaces (`React.Key` through `React`, imported or global), or
|
|
626
|
+
* undefined.
|
|
627
|
+
* @param {JudgeContext} ctx
|
|
628
|
+
* @param {string} name
|
|
629
|
+
* @returns {any}
|
|
630
|
+
*/
|
|
631
|
+
function resolveSymbol(ctx, name) {
|
|
632
|
+
const {ts, checker} = ctx;
|
|
633
|
+
const meaning =
|
|
634
|
+
ts.SymbolFlags.Type | ts.SymbolFlags.Alias | ts.SymbolFlags.Namespace;
|
|
635
|
+
const target = (/** @type {any} */ symbol) =>
|
|
636
|
+
symbol && symbol.flags & ts.SymbolFlags.Alias
|
|
637
|
+
? checker.getAliasedSymbol(symbol)
|
|
638
|
+
: symbol;
|
|
639
|
+
try {
|
|
640
|
+
const [head, ...tail] = name.split('.');
|
|
641
|
+
// A module's own declarations can come back as local symbols without
|
|
642
|
+
// flags: take the export symbol, and the first one that names a type or
|
|
643
|
+
// namespace.
|
|
644
|
+
let symbol = checker
|
|
645
|
+
.getSymbolsInScope(ctx.location, meaning)
|
|
646
|
+
.filter(/** @param {any} s */ s => s.name === head)
|
|
647
|
+
.map(/** @param {any} s */ s => checker.getExportSymbolOfSymbol(s))
|
|
648
|
+
.find(/** @param {any} s */ s => (s.flags & meaning) !== 0);
|
|
649
|
+
for (const part of tail) {
|
|
650
|
+
symbol = symbol
|
|
651
|
+
? checker
|
|
652
|
+
.getExportsOfModule(target(symbol))
|
|
653
|
+
.find(/** @param {any} s */ s => s.name === part)
|
|
654
|
+
: undefined;
|
|
655
|
+
}
|
|
656
|
+
return target(symbol);
|
|
657
|
+
} catch {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
}
|