Skip to content

Commit

Permalink
[docs] Include default values in IntelliSense (mui#22447)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Sep 4, 2020
1 parent 1a4495b commit d9ca53d
Show file tree
Hide file tree
Showing 222 changed files with 1,191 additions and 80 deletions.
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
151 changes: 98 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,71 @@ 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) {
throw new Error(`JSDOC @default annotation not found for '${propName}'.`);
}
} 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 +364,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

0 comments on commit d9ca53d

Please sign in to comment.