[docs-infra] Fix duplicate JSDoc in proptypes generation#48304
[docs-infra] Fix duplicate JSDoc in proptypes generation#48304brijeshb42 wants to merge 2 commits intomui:masterfrom
Conversation
| symbol, | ||
| location, | ||
| typeStack, | ||
| parentType, |
There was a problem hiding this comment.
wasnt being used in the function.
Bundle size
Deploy previewhttps://deploy-preview-48304--material-ui.netlify.app/ Check out the code infra dashboard for more information about this PR. |
There was a problem hiding this comment.
Pull request overview
Fixes duplicated JSDoc text in generated propTypes by changing getSymbolDocumentation to prefer a single declaration (“last declaration wins”) when a symbol has multiple declarations (e.g., intersections, declaration merging/module augmentation), aligning with existing squashPropTypeDefinitions behavior.
Changes:
- Update
getSymbolDocumentationto return the last declaration’s JSDoc instead of concatenating/deduplicating across declarations. - Add fixture-based test cases covering intersections, interface extends, interface overrides, and declaration merging/module augmentation.
- Add a declaration-merging fixture that validates augmented JSDoc is selected.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages-internal/scripts/typescript-to-proptypes/src/getPropTypesFromFile.ts | Changes symbol JSDoc selection strategy to avoid duplicated descriptions in generated propTypes. |
| packages-internal/scripts/typescript-to-proptypes/test/jsdoc-intersection/input.tsx | New intersection fixture input to exercise multi-declaration JSDoc handling. |
| packages-internal/scripts/typescript-to-proptypes/test/jsdoc-intersection/output.js | Expected output asserting “last declaration wins” for intersection JSDoc. |
| packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface-extends/input.tsx | New interface-extends fixture input for JSDoc behavior verification. |
| packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface-extends/output.js | Expected output for interface-extends scenario. |
| packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface-override/input.tsx | New interface override fixture input. |
| packages-internal/scripts/typescript-to-proptypes/test/jsdoc-interface-override/output.js | Expected output for interface override scenario. |
| packages-internal/scripts/typescript-to-proptypes/test/declaration-merging/input.tsx | New declaration-merging fixture component that imports merged types. |
| packages-internal/scripts/typescript-to-proptypes/test/declaration-merging/types.ts | Declaration merging/module augmentation fixture that defines competing JSDoc. |
| packages-internal/scripts/typescript-to-proptypes/test/declaration-merging/output.js | Expected output asserting augmented JSDoc is selected. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // In all cases we use the last declaration's JSDoc. This is consistent with | ||
| // squashPropTypeDefinitions and avoids duplicated descriptions when multiple | ||
| // declarations carry different JSDoc (e.g. module augmentation overriding | ||
| // the original, or intersection types where the last constituent's JSDoc | ||
| // is the most specific). |
There was a problem hiding this comment.
The comment says "use the last declaration's JSDoc", but the implementation filters out declarations without JSDoc first and then picks the last commented declaration. If the last declaration has no JSDoc, an earlier comment will be used, so the comment is currently inaccurate—either adjust the wording (e.g. "last declaration that has JSDoc wins") or change the logic to truly prefer the last declaration even when it has no JSDoc (falling back to symbol.getDocumentationComment only when none have JSDoc).
| /** | ||
| * The label from extra props. | ||
| * @default 'extra' | ||
| */ |
There was a problem hiding this comment.
I think this is wrong.
IMO (the previous behaviour) is what the user would see in the UI, and the better approach since the developer can see there is something wrong with the declarations.
The expected value here should be something like
/**
* The label of the component. The label from extra props.
* @default 'base'
* @default 'extra'
*/
There was a problem hiding this comment.
This was the main change that I did in this new pr actually, because this is what I was getting in X in one file -
/**
* If `true`, the `input` element is focused during the first mount.
* @default false
* If `true`, the field is focused during the first mount.
* @default false
*/
autoFocus: PropTypes.bool,
another file -
/**
* Callback fired when a bar item is clicked.
* @param {MouseEvent} event The event source of the callback.
* @param {BarItemIdentifier} barItemIdentifier The bar item identifier.
* Callback fired when a bar item is clicked.
* @param {MouseEvent | React.MouseEvent<SVGElement, MouseEvent>} event The event source of the callback.
* It is a native MouseEvent for `svg-batch` renderer and a React MouseEvent for `svg-single` renderer.
* @param {BarItemIdentifier} barItemIdentifier The bar item identifier.
*/
onItemClick: PropTypes.func,the above change makes the onItemClick look like it has 4 params.
4ed6eae to
4c35863
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4c35863 to
bd4a4ae
Compare
`getSymbolDocumentation` (introduced in mui#47685) concatenated JSDoc from all declarations when a symbol had multiple (intersection, declaration merging, module augmentation). This produced duplicated descriptions in generated propTypes. Fix: use "last declaration wins" for all multi-declaration cases, consistent with `squashPropTypeDefinitions`. Added tests for intersection, interface extends, interface override, and declaration merging scenarios. This change has no effect on this repo and affects around 7 api/proptypes generation in X repo.
bd4a4ae to
cf9ca97
Compare
When a symbol has multiple declarations (intersection, declaration merging, module augmentation), pick the JSDoc comment with the richest @param signature (highest count of union-constituent types) instead of blindly using first/last declaration. Ties break to last for intersection (override semantics), first for declaration merging. This matches how VS Code hover surfaces the more-complete overloaded signature and preserves the augmentation's widened types (e.g. BarPlotProps onItemClick where the module augmentation widens MouseEvent to MouseEvent | React.MouseEvent). Added tests: jsdoc-intersection-params, jsdoc-intersection-same-type, jsdoc-intersection-partial, jsdoc-declaration-merging-func. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Matches VS Code hover behavior: | ||
| // 1. Intersection (type C = A & B): append unique JSDoc from all constituents | ||
| // 2. Interface extends (interface Z extends X, Y): single declaration, use it | ||
| // 3. Interface override (interface W extends X { prop }): single declaration, use it | ||
| // 4. Declaration merging / module augmentation: first declaration that has JSDoc wins | ||
| const comments = decl |
There was a problem hiding this comment.
The inline comment describing the behavior for intersections says “append unique JSDoc from all constituents”, but the implementation now selects a single comment via pickCoveringJSDoc. Please update the comment to match the actual behavior to avoid future confusion when maintaining this logic.
| // Pick the declaration with the most-covering JSDoc (richest @param | ||
| // signature), matching how VS Code hover shows overloaded signatures. | ||
| // For intersections, ties resolve to the last constituent (override semantics); | ||
| // for declaration merging / extends, ties resolve to the first. | ||
| const tieBreak = parentType && parentType.isIntersection() ? 'last' : 'first'; | ||
| return pickCoveringJSDoc(comments, tieBreak); |
There was a problem hiding this comment.
PR description says the fix is “last declaration wins” for multi-declaration symbols, but the implementation chooses the “most covering” JSDoc by counting @param union constituents and only uses “last” as a tie-break for intersections. If “last declaration wins” is the intended contract, this selection logic (and the tieBreak choice of first for non-intersections) should be adjusted; otherwise, please update the PR description to reflect the new selection rule.
| */ | ||
| function flattenUnionTypes(type: any): string[] { | ||
| if (type?.type === 'UnionType') { | ||
| return type.elements.flatMap(flattenUnionTypes); | ||
| } | ||
| const s = stringifyDoctrineType(type); | ||
| return s ? [s] : []; |
There was a problem hiding this comment.
flattenUnionTypes only flattens when the top-level doctrine node is UnionType. For common JSDoc forms like {(A|B)=} / {? (A|B)} / {... (A|B)} doctrine wraps the union in OptionalType/NullableType/RestType, so this will undercount constituents (treating it as 1) and can pick the wrong “most covering” doc. Consider making flattenUnionTypes recurse through wrapper nodes (expression, applications, etc.) and only treat leaves as one constituent when they’re not unions.
| */ | |
| function flattenUnionTypes(type: any): string[] { | |
| if (type?.type === 'UnionType') { | |
| return type.elements.flatMap(flattenUnionTypes); | |
| } | |
| const s = stringifyDoctrineType(type); | |
| return s ? [s] : []; | |
| * Recurses through doctrine wrapper/container nodes so unions nested in | |
| * OptionalType / NullableType / RestType / TypeApplication / ArrayType | |
| * are counted by their actual constituent leaves. | |
| */ | |
| function flattenUnionTypes(type: any): string[] { | |
| if (!type) { | |
| return []; | |
| } | |
| switch (type.type) { | |
| case 'UnionType': | |
| return type.elements.flatMap(flattenUnionTypes); | |
| case 'OptionalType': | |
| case 'NullableType': | |
| case 'NonNullableType': | |
| case 'RestType': | |
| return flattenUnionTypes(type.expression); | |
| case 'TypeApplication': | |
| return [ | |
| ...flattenUnionTypes(type.expression), | |
| ...type.applications.flatMap(flattenUnionTypes), | |
| ]; | |
| case 'ArrayType': | |
| return type.elements.flatMap(flattenUnionTypes); | |
| default: { | |
| const s = stringifyDoctrineType(type); | |
| return s ? [s] : []; | |
| } | |
| } |
| const parsed = comments.map((c) => | ||
| doctrine.parse( | ||
| `/**\n${c | ||
| .split('\n') | ||
| .map((l) => ` * ${l}`) | ||
| .join('\n')}\n */`, | ||
| { unwrap: true }, | ||
| ), | ||
| ); | ||
| const paramTypeCount = (p: (typeof parsed)[number]) => | ||
| p.tags | ||
| .filter((t) => t.title === 'param') | ||
| .reduce((sum, t) => sum + (t.type ? flattenUnionTypes(t.type).length : 0), 0); | ||
|
|
||
| let bestIndex = 0; | ||
| let bestCount = paramTypeCount(parsed[0]); | ||
| for (let i = 1; i < parsed.length; i += 1) { | ||
| const count = paramTypeCount(parsed[i]); |
There was a problem hiding this comment.
pickCoveringJSDoc calls doctrine.parse(...) on every candidate comment without guarding against parse failures. If any JSDoc contains syntax doctrine doesn’t handle, this will throw and abort propTypes generation for the whole file. Consider wrapping parsing in a try/catch and falling back to a deterministic tie-break (e.g. first/last) when parsing fails (or skipping parse when no @param tags are present).
| const parsed = comments.map((c) => | |
| doctrine.parse( | |
| `/**\n${c | |
| .split('\n') | |
| .map((l) => ` * ${l}`) | |
| .join('\n')}\n */`, | |
| { unwrap: true }, | |
| ), | |
| ); | |
| const paramTypeCount = (p: (typeof parsed)[number]) => | |
| p.tags | |
| .filter((t) => t.title === 'param') | |
| .reduce((sum, t) => sum + (t.type ? flattenUnionTypes(t.type).length : 0), 0); | |
| let bestIndex = 0; | |
| let bestCount = paramTypeCount(parsed[0]); | |
| for (let i = 1; i < parsed.length; i += 1) { | |
| const count = paramTypeCount(parsed[i]); | |
| const getParamTypeCount = (comment: string) => { | |
| if (!/@param\b/.test(comment)) { | |
| return 0; | |
| } | |
| try { | |
| const parsed = doctrine.parse( | |
| `/**\n${comment | |
| .split('\n') | |
| .map((l) => ` * ${l}`) | |
| .join('\n')}\n */`, | |
| { unwrap: true }, | |
| ); | |
| return parsed.tags | |
| .filter((t) => t.title === 'param') | |
| .reduce((sum, t) => sum + (t.type ? flattenUnionTypes(t.type).length : 0), 0); | |
| } catch { | |
| return 0; | |
| } | |
| }; | |
| let bestIndex = 0; | |
| let bestCount = getParamTypeCount(comments[0]); | |
| for (let i = 1; i < comments.length; i += 1) { | |
| const count = getParamTypeCount(comments[i]); |
getSymbolDocumentation(introduced in #47685) concatenated JSDoc fromall declarations when a symbol had multiple (intersection, declaration
merging, module augmentation). This produced duplicated descriptions in
generated propTypes.
Fix: use "last declaration wins" for all multi-declaration cases,
consistent with
squashPropTypeDefinitions.Added tests for intersection, interface extends, interface override,
and declaration merging scenarios.
This change has no effect on this repo and affects around 7 api/proptypes generation in X repo.