UNPKG

eslint-doc-generator

Version:

Automatic documentation generator for ESLint plugins and rules.

98 lines (97 loc) 4.35 kB
import traverse from 'json-schema-traverse'; import { getProperty, hasProperty } from 'dot-prop'; import { capitalizeOnlyFirstLetter } from './string.js'; function typeToString(type) { return Array.isArray(type) ? type.map((item) => capitalizeOnlyFirstLetter(item)).join(', ') : capitalizeOnlyFirstLetter(type); } function hasObjectPath(object, path) { return ((Array.isArray(object) || (typeof object === 'object' && object !== null)) && hasProperty(object, path)); } function getCurrentOptionPath(parentPath, parentKeyword, keyIndex) { if (parentKeyword === 'properties' && typeof keyIndex === 'string') { return [...(parentPath ?? []), keyIndex]; } if (parentKeyword === 'items' && typeof keyIndex === 'number') { return [...(parentPath ?? []), keyIndex]; } return parentPath ?? []; } function collectNamedOptionsFromSchemaProperties(js, baseOptionPath, metaDefaultOptionForSchema, options) { for (const [key, value] of Object.entries(js.properties ?? {})) { const type = value.type === 'array' && !Array.isArray(value.items) && value.items?.type ? `${Array.isArray(value.items.type) && value.items.type.length > 1 ? `(${typeToString(value.items.type)})` : typeToString(value.items.type)}[]` : value.type ? typeToString(value.type) : undefined; const required = typeof value.required === 'boolean' ? value.required : Array.isArray(js.required) && js.required.includes(key); // Property exists on future JSONSchema version but we can let it be used anyway. const deprecated = 'deprecated' in value ? Boolean(value['deprecated']) : false; // Prefer `meta.defaultOptions` over schema `default`. let defaultValue = value.default; const optionPath = [...baseOptionPath, key]; if (hasObjectPath(metaDefaultOptionForSchema, optionPath)) { defaultValue = getProperty(metaDefaultOptionForSchema, optionPath); } const option = { name: key, ...(type !== undefined && { type }), ...(value.description !== undefined && { description: value.description, }), ...(defaultValue !== undefined && { default: defaultValue }), ...(value.enum !== undefined && { enum: value.enum }), ...(required && { required }), ...(deprecated && { deprecated }), }; options.push(option); } } /** * Gather a list of named options from a rule schema. * @param jsonSchema - the JSON schema to check * @param metaDefaultOptions - the `meta.defaultOptions`, if any * @returns - list of named options we could detect from the schema */ export function getAllNamedOptions(jsonSchema, metaDefaultOptions) { if (!jsonSchema) { return []; } if (Array.isArray(jsonSchema)) { const metaDefaultOptionsForItems = Array.isArray(metaDefaultOptions) ? metaDefaultOptions : undefined; return jsonSchema.flatMap((js, index) => getAllNamedOptions(js, metaDefaultOptionsForItems?.[index])); } const options = []; const optionPathBySchema = new WeakMap([[jsonSchema, []]]); traverse( // Cast needed because json-schema-traverse types don't account for exactOptionalPropertyTypes jsonSchema, (js, _jsonPtr, _rootSchema, _parentJsonPtr, parentKeyword, parentSchema, keyIndex) => { const parentPath = parentSchema && optionPathBySchema.has(parentSchema) ? optionPathBySchema.get(parentSchema) : []; const currentPath = getCurrentOptionPath(parentPath, parentKeyword, keyIndex); optionPathBySchema.set(js, currentPath); if (js.properties) { collectNamedOptionsFromSchemaProperties(js, currentPath, metaDefaultOptions, options); } }); return options; } /** * Check if a rule schema is non-blank/empty and thus has actual options. * @param jsonSchema - the JSON schema to check * @returns - whether the schema has options */ export function hasOptions(jsonSchema) { return ((Array.isArray(jsonSchema) && jsonSchema.length > 0) || (typeof jsonSchema === 'object' && Object.keys(jsonSchema).length > 0)); }