From 245ac2343d7e4d4cd9b3cd7c4e43d0a81f10d08d Mon Sep 17 00:00:00 2001 From: "yuxuan.weng" Date: Fri, 23 Oct 2026 13:53:41 +0800 Subject: [PATCH] fix: prefer descriptions on overriding type properties ref:0 --- .changeset/clear-inherited-descriptions.md | 5 ++ .../src/handlers/codeTypeHandler.ts | 55 +++++++++++++++++-- .../tests/integration/integration-test.ts | 47 ++++++++++++++++ 3 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 .changeset/clear-inherited-descriptions.md diff --git a/.changeset/clear-inherited-descriptions.md b/.changeset/clear-inherited-descriptions.md new file mode 100644 index 00000000000..9efdfd7a033 --- /dev/null +++ b/.changeset/clear-inherited-descriptions.md @@ -0,0 +1,5 @@ +--- +'react-docgen': patch +--- + +Use the overriding type property's description when it provides one, while preserving inherited descriptions for undocumented overrides and descriptions supplied by higher-priority handlers. diff --git a/packages/react-docgen/src/handlers/codeTypeHandler.ts b/packages/react-docgen/src/handlers/codeTypeHandler.ts index 75310b4b3d8..7cfd6390e14 100644 --- a/packages/react-docgen/src/handlers/codeTypeHandler.ts +++ b/packages/react-docgen/src/handlers/codeTypeHandler.ts @@ -10,14 +10,40 @@ import type { TypeParameters } from '../utils/getTypeParameters.js'; import resolveToValue from '../utils/resolveToValue.js'; import setPropDescription from '../utils/setPropDescription.js'; import type { NodePath } from '@babel/traverse'; -import type { FlowType } from '@babel/types'; +import type { + FlowType, + ObjectTypeProperty, + TSPropertySignature, +} from '@babel/types'; +import { getDocblock } from '../utils/docblock.js'; import type { ComponentNode } from '../resolver/index.js'; import type { Handler } from './index.js'; +function setTypePropDescription( + documentation: Documentation, + path: NodePath, + describedProps: Set, +): void { + const propName = getPropertyName(path); + + if (!propName) return; + const descriptor = documentation.getPropDescriptor(propName); + + // Only replace descriptions produced by this handler, never higher-priority handlers. + if (describedProps.has(propName) && getDocblock(path)) { + descriptor.description = ''; + } + const hadDescription = Boolean(descriptor.description); + + setPropDescription(documentation, path); + if (!hadDescription) describedProps.add(propName); +} + function setPropDescriptor( documentation: Documentation, path: NodePath, typeParams: TypeParameters | null, + describedProps: Set, ): void { if (path.isObjectTypeSpreadProperty()) { const argument = unwrapUtilityType( @@ -29,7 +55,12 @@ function setPropDescriptor( documentation, argument, (propertyPath, innerTypeParams) => { - setPropDescriptor(documentation, propertyPath, innerTypeParams); + setPropDescriptor( + documentation, + propertyPath, + innerTypeParams, + describedProps, + ); }, typeParams, ); @@ -51,7 +82,12 @@ function setPropDescriptor( documentation, right, (propertyPath, innerTypeParams) => { - setPropDescriptor(documentation, propertyPath, innerTypeParams); + setPropDescriptor( + documentation, + propertyPath, + innerTypeParams, + describedProps, + ); }, typeParams, ); @@ -72,7 +108,7 @@ function setPropDescriptor( // We are doing this here instead of in a different handler // to not need to duplicate the logic for checking for // imported types that are spread in to props. - setPropDescription(documentation, path); + setTypePropDescription(documentation, path, describedProps); } else if (path.isTSPropertySignature()) { const typeAnnotation = path.get('typeAnnotation'); @@ -93,7 +129,7 @@ function setPropDescriptor( // We are doing this here instead of in a different handler // to not need to duplicate the logic for checking for // imported types that are spread in to props. - setPropDescription(documentation, path); + setTypePropDescription(documentation, path, describedProps); } } @@ -112,12 +148,19 @@ const codeTypeHandler: Handler = function ( return; } + const describedProps = new Set(); + for (const typePath of typePaths) { applyToTypeProperties( documentation, typePath, (propertyPath, typeParams) => { - setPropDescriptor(documentation, propertyPath, typeParams); + setPropDescriptor( + documentation, + propertyPath, + typeParams, + describedProps, + ); }, null, ); diff --git a/packages/react-docgen/tests/integration/integration-test.ts b/packages/react-docgen/tests/integration/integration-test.ts index 48d0ba83ded..d00861cb569 100644 --- a/packages/react-docgen/tests/integration/integration-test.ts +++ b/packages/react-docgen/tests/integration/integration-test.ts @@ -5,6 +5,53 @@ import { parse, makeFsImporter } from '../../src/main'; import { fileURLToPath } from 'url'; describe('integration', () => { + test.each([ + ['/** child */', 'child'], + ['', 'parent'], + ])( + 'uses an overriding property description when present: %s', + (comment, expected) => { + const result = parse( + `interface Parent { /** parent */ value: any; } + interface Props extends Parent { ${comment} value: string; } + export default function Example(props: Props) { return
; }`, + { babelOptions: { filename: 'inherited.tsx', babelrc: false } }, + ); + + expect(result[0]?.props?.value).toMatchObject({ + description: expected, + tsType: { name: 'string' }, + }); + }, + ); + + test('preserves a propTypes description over inherited type comments', () => { + const result = parse( + `import PropTypes from 'prop-types'; + interface Parent { /** parent */ value: any; } + interface Props extends Parent { /** child */ value: string; } + export default function Example(props: Props) { return
; } + Example.propTypes = { /** runtime */ value: PropTypes.string };`, + { babelOptions: { filename: 'inherited.tsx', babelrc: false } }, + ); + + expect(result[0]?.props?.value?.description).toBe('runtime'); + }); + + test('uses the overriding description after a Flow type spread', () => { + const result = parse( + `type Parent = { /** parent */ value: any }; + type Props = { ...Parent, /** child */ value: string }; + export default function Example(props: Props) { return
; }`, + { babelOptions: { filename: 'inherited.js', babelrc: false } }, + ); + + expect(result[0]?.props?.value).toMatchObject({ + description: 'child', + flowType: { name: 'string' }, + }); + }); + describe('fixtures', () => { const fixturePath = join( dirname(fileURLToPath(import.meta.url)),