Skip to content

[docs-infra] Fix duplicate JSDoc in proptypes generation#48304

Open
brijeshb42 wants to merge 2 commits intomui:masterfrom
brijeshb42:fix/simplify-jsdoc-intersection-handling
Open

[docs-infra] Fix duplicate JSDoc in proptypes generation#48304
brijeshb42 wants to merge 2 commits intomui:masterfrom
brijeshb42:fix/simplify-jsdoc-intersection-handling

Conversation

@brijeshb42
Copy link
Copy Markdown
Contributor

getSymbolDocumentation (introduced in #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.

@brijeshb42 brijeshb42 requested a review from JCQuintas April 16, 2026 12:10
@brijeshb42 brijeshb42 added the scope: docs-infra Involves the docs-infra product (https://www.notion.so/mui-org/b9f676062eb94747b6768209f7751305). label Apr 16, 2026
symbol,
location,
typeStack,
parentType,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasnt being used in the function.

@code-infra-dashboard
Copy link
Copy Markdown

code-infra-dashboard bot commented Apr 16, 2026

Bundle size

Bundle Parsed size Gzip size
@mui/material 0B(0.00%) 0B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes

Deploy preview

https://deploy-preview-48304--material-ui.netlify.app/


Check out the code infra dashboard for more information about this PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 getSymbolDocumentation to 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.

Comment on lines +54 to +58
// 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).
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
/**
* The label from extra props.
* @default 'extra'
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
   */

Copy link
Copy Markdown
Contributor Author

@brijeshb42 brijeshb42 Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@brijeshb42 brijeshb42 requested review from a team and JCQuintas April 17, 2026 05:19
@brijeshb42 brijeshb42 force-pushed the fix/simplify-jsdoc-intersection-handling branch from 4ed6eae to 4c35863 Compare April 17, 2026 05:26
@brijeshb42 brijeshb42 requested a review from Copilot April 17, 2026 06:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages-internal/scripts/typescript-to-proptypes/src/getPropTypesFromFile.ts Outdated
@brijeshb42 brijeshb42 force-pushed the fix/simplify-jsdoc-intersection-handling branch from 4c35863 to bd4a4ae Compare April 17, 2026 06:27
`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.
@brijeshb42 brijeshb42 force-pushed the fix/simplify-jsdoc-intersection-handling branch from bd4a4ae to cf9ca97 Compare April 17, 2026 06:31
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +144 to +149
// 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
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +168
// 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);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +39
*/
function flattenUnionTypes(type: any): string[] {
if (type?.type === 'UnionType') {
return type.elements.flatMap(flattenUnionTypes);
}
const s = stringifyDoctrineType(type);
return s ? [s] : [];
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
*/
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] : [];
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +116
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]);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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]);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs-infra Involves the docs-infra product (https://www.notion.so/mui-org/b9f676062eb94747b6768209f7751305).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants