Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs] Include default values in IntelliSense #22447

Merged
merged 13 commits into from
Sep 4, 2020
2 changes: 2 additions & 0 deletions docs/scripts/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ declare module 'react-docgen' {

export interface PropDescriptor {
defaultValue?: { computed: boolean; value: string };
// augmented by docs/src/modules/utils/defaultPropsHandler.js
jsdocDefaultValue?: { computed: boolean; value: string };
description?: string;
required?: boolean;
/**
Expand Down
15 changes: 6 additions & 9 deletions docs/src/modules/utils/defaultPropsHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,13 @@ function getDefaultValuesFromProps(properties, documentation) {
sloppy: true,
}),
);
const defaultValue = getDefaultValue(propertyPath);

if (jsdocDefaultValue != null && defaultValue != null) {
throw new Error(
`Can't have JavaScript default value and jsdoc @defaultValue in prop '${propName}'. Remove the @defaultValue if you need the JavaScript default value at runtime.`,
);
if (jsdocDefaultValue) {
propDescriptor.jsdocDefaultValue = jsdocDefaultValue;
}
const usedDefaultValue = defaultValue || jsdocDefaultValue;
if (usedDefaultValue) {
propDescriptor.defaultValue = usedDefaultValue;

const defaultValue = getDefaultValue(propertyPath);
if (defaultValue) {
propDescriptor.defaultValue = defaultValue;
}
});
}
Expand Down
155 changes: 102 additions & 53 deletions docs/src/modules/utils/generateMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import {
import { SOURCE_CODE_ROOT_URL, LANGUAGES_IN_PROGRESS } from 'docs/src/modules/constants';
import { pageToTitle } from './helpers';

interface DescribeablePropDescriptor {
annotation: doctrine.Annotation;
defaultValue: string | null;
required: boolean;
type: PropTypeDescriptor;
}

export interface ReactApi extends ReactDocgenApi {
EOL: string;
filename: string;
Expand Down Expand Up @@ -121,8 +128,8 @@ function resolveType(type: NonNullable<doctrine.Tag['type']>): string {
throw new TypeError(`resolveType for '${type.type}' not implemented`);
}

function generatePropDescription(prop: PropDescriptor) {
const { description } = prop;
function generatePropDescription(prop: DescribeablePropDescriptor, propName: string): string {
const { annotation } = prop;
const type = prop.type;
let deprecated = '';

Expand All @@ -133,57 +140,42 @@ function generatePropDescription(prop: PropDescriptor) {
}
}

if (description === undefined) {
throw new Error('wrong doctrine#parse type');
}
const parsed = doctrine.parse(description, {
sloppy: true,
});

// Two new lines result in a newline in the table.
// All other new lines must be eliminated to prevent markdown mayhem.
const jsDocText = escapeCell(parsed.description)
const jsDocText = escapeCell(annotation.description)
.replace(/(\r?\n){2}/g, '<br>')
.replace(/\r?\n/g, ' ');

if (parsed.tags.some((tag) => tag.title === 'ignore')) {
return null;
}

let signature = '';

if (type.name === 'func' && parsed.tags.length > 0) {
// Split up the parsed tags into 'arguments' and 'returns' parsed objects. If there's no
// 'returns' parsed object (i.e., one with title being 'returns'), make one of type 'void'.
const parsedArgs: doctrine.Tag[] = annotation.tags.filter((tag) => tag.title === 'param');
let parsedReturns:
| doctrine.Tag
| { description?: undefined; type: { name: string } }
| undefined = annotation.tags.find((tag) => tag.title === 'returns');
if (type.name === 'func' && (parsedArgs.length > 0 || parsedReturns !== undefined)) {
parsedReturns = parsedReturns ?? { type: { name: 'void' } };

// Remove new lines from tag descriptions to avoid markdown errors.
parsed.tags.forEach((tag) => {
annotation.tags.forEach((tag) => {
if (tag.description) {
tag.description = tag.description.replace(/\r*\n/g, ' ');
}
});

// Split up the parsed tags into 'arguments' and 'returns' parsed objects. If there's no
// 'returns' parsed object (i.e., one with title being 'returns'), make one of type 'void'.
const parsedLength = parsed.tags.length;
let parsedArgs: doctrine.Tag[] = [];
let parsedReturns: doctrine.Tag;

if (parsed.tags[parsedLength - 1].title === 'returns') {
parsedArgs = parsed.tags.slice(0, parsedLength - 1);
parsedReturns = parsed.tags[parsedLength - 1];
} else {
parsedArgs = parsed.tags;
// @ts-expect-error
parsedReturns = { type: { name: 'void' } };
}

signature += '<br><br>**Signature:**<br>`function(';
signature += parsedArgs
.map((tag) => {
.map((tag, index) => {
if (tag.type != null && tag.type.type === 'OptionalType') {
return `${tag.name}?: ${(tag.type.expression as any).name}`;
}

if (tag.type === undefined) {
throw new TypeError('Tag has no type');
throw new TypeError(
`In function signature for prop '${propName}' Argument #${index} has no type.`,
);
}
return `${tag.name}: ${resolveType(tag.type!)}`;
})
Expand Down Expand Up @@ -298,6 +290,75 @@ The \`${reactAPI.styles.name}\` name can be used for providing [default props](/
`;
}

/**
* Returns `null` if the prop should be ignored.
* Throws if it is invalid.
*
* @param prop
* @param propName
*/
function createDescribeableProp(
prop: PropDescriptor,
propName: string,
): DescribeablePropDescriptor | null {
const { defaultValue, jsdocDefaultValue, description, required, type } = prop;

const renderedDefaultValue = defaultValue?.value.replace(/\r?\n/g, '');
const renderDefaultValue = Boolean(
renderedDefaultValue &&
// Ignore "large" default values that would break the table layout.
renderedDefaultValue.length <= 150,
);

if (description === undefined) {
throw new Error(`The "${propName}" prop is missing a description.`);
}

const annotation = doctrine.parse(description, {
sloppy: true,
});

if (
annotation.description.trim() === '' ||
annotation.tags.some((tag) => tag.title === 'ignore')
) {
return null;
}

if (jsdocDefaultValue !== undefined && defaultValue === undefined) {
throw new Error(
`Declared a @default annotation in JSDOC for prop '${propName}' but could not find a default value in the implementation.`,
);
} else if (jsdocDefaultValue === undefined && defaultValue !== undefined && renderDefaultValue) {
const shouldHaveDefaultAnnotation =
// Discriminator for polymorphism which is not documented at the component level.
// The documentation of `component` does not know in which component it is used.
propName !== 'component';

if (shouldHaveDefaultAnnotation) {
// TODO: throw/warn/ignore?
// throw new Error(
// `JSDOC @default annotation not found for '${propName}'.`,
// );
console.warn(`JSDOC @default annotation not found for '${propName}'.`);
eps1lon marked this conversation as resolved.
Show resolved Hide resolved
}
} else if (jsdocDefaultValue !== undefined) {
// `defaultValue` can't be undefined or we would've thrown earlier.
if (jsdocDefaultValue.value !== defaultValue!.value) {
throw new Error(
`Expected JSDOC @default annotation for prop '${propName}' of "${jsdocDefaultValue.value}" to equal runtime default value of "${defaultValue?.value}"`,
);
}
}

return {
annotation,
defaultValue: renderDefaultValue ? renderedDefaultValue! : null,
required: Boolean(required),
type,
};
}

function generateProps(reactAPI: ReactApi) {
const header = '## Props';

Expand All @@ -307,34 +368,22 @@ function generateProps(reactAPI: ReactApi) {
|:-----|:-----|:--------|:------------|\n`;

Object.keys(reactAPI.props).forEach((propName) => {
const prop = reactAPI.props[propName];

if (typeof prop.description === 'undefined') {
throw new Error(`The "${propName}" prop is missing a description`);
}

const propDescriptor = reactAPI.props[propName];
if (propName === 'classes') {
prop.description += ' See [CSS API](#css) below for more details.';
propDescriptor.description += ' See [CSS API](#css) below for more details.';
}

const description = generatePropDescription(prop);

if (description === null) {
const prop = createDescribeableProp(propDescriptor, propName);
if (prop === null) {
return;
}

const renderedDefaultValue = prop.defaultValue?.value.replace(/\r*\n/g, '');
const renderDefaultValue =
renderedDefaultValue &&
// Ignore "large" default values that would break the table layout.
renderedDefaultValue.length <= 150;
const description = generatePropDescription(prop, propName);

let defaultValueColumn = '';
if (renderDefaultValue) {
defaultValueColumn = `<span class="prop-default">${escapeCell(
// narrowed `renderedDefaultValue` to non-nullable by `renderDefaultValue`
renderedDefaultValue!,
)}</span>`;
// give up on "large" default values e.g. big functions or objects
if (prop.defaultValue) {
defaultValueColumn = `<span class="prop-default">${escapeCell(prop.defaultValue!)}</span>`;
}

const chainedPropType = getChained(prop.type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ CircularProgressWithLabel.propTypes = {
/**
* The value of the progress indicator for the determinate variant.
* Value between 0 and 100.
* @default 0
*/
value: PropTypes.number.isRequired,
};
Expand Down
4 changes: 4 additions & 0 deletions docs/src/pages/components/steppers/CustomizedSteppers.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ function QontoStepIcon(props) {
QontoStepIcon.propTypes = {
/**
* Whether this step is active.
* @default false
*/
active: PropTypes.bool,
/**
* Mark the step as completed. Is passed to child components.
* @default false
*/
completed: PropTypes.bool,
};
Expand Down Expand Up @@ -161,10 +163,12 @@ function ColorlibStepIcon(props) {
ColorlibStepIcon.propTypes = {
/**
* Whether this step is active.
* @default false
*/
active: PropTypes.bool,
/**
* Mark the step as completed. Is passed to child components.
* @default false
*/
completed: PropTypes.bool,
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Typography.propTypes = {
marked: PropTypes.oneOf(['center', 'left', 'none']),
/**
* Applies the theme typography styles.
* @default 'body1'
*/
variant: PropTypes.oneOf([
'body1',
Expand Down
4 changes: 4 additions & 0 deletions packages/material-ui-lab/src/Alert/Alert.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface AlertProps extends StandardProps<PaperProps, 'variant'> {
* Override the default label for the *close popup* icon button.
*
* For localization purposes, you can use the provided [translations](/guides/localization/).
* @default 'Close'
*/
closeText?: string;
/**
Expand All @@ -68,6 +69,7 @@ export interface AlertProps extends StandardProps<PaperProps, 'variant'> {
color?: Color;
/**
* The severity of the alert. This defines the color and icon used.
* @default 'success'
*/
severity?: Color;
/**
Expand All @@ -77,6 +79,7 @@ export interface AlertProps extends StandardProps<PaperProps, 'variant'> {
icon?: React.ReactNode | false;
/**
* The ARIA role attribute of the element.
* @default 'alert'
*/
role?: string;
/**
Expand All @@ -95,6 +98,7 @@ export interface AlertProps extends StandardProps<PaperProps, 'variant'> {
onClose?: (event: React.SyntheticEvent) => void;
/**
* The variant to use.
* @default 'standard'
*/
variant?: OverridableStringUnion<AlertVariantDefaults, AlertPropsVariantOverrides>;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/material-ui-lab/src/Alert/Alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ Alert.propTypes = {
* Override the default label for the *close popup* icon button.
*
* For localization purposes, you can use the provided [translations](/guides/localization/).
* @default 'Close'
*/
closeText: PropTypes.string,
/**
Expand Down Expand Up @@ -274,14 +275,17 @@ Alert.propTypes = {
onClose: PropTypes.func,
/**
* The ARIA role attribute of the element.
* @default 'alert'
*/
role: PropTypes.string,
/**
* The severity of the alert. This defines the color and icon used.
* @default 'success'
*/
severity: PropTypes.oneOf(['error', 'info', 'success', 'warning']),
/**
* The variant to use.
* @default 'standard'
*/
variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
PropTypes.oneOf(['filled', 'outlined', 'standard']),
Expand Down
Loading