UNPKG

eslint-plugin-jsdoc

Version:
671 lines (625 loc) 28.4 kB
import iterateJsdoc from '../iterateJsdoc.js'; import { rewireByParsedType, } from '../jsdocUtils.js'; import { parse as parseType, stringify, traverse, tryParse as tryParseType, } from '@es-joy/jsdoccomment'; const digitRegex = (/^(\d+(\.\d*)?|\.\d+)([eE][\-+]?\d+)?$/v); export default iterateJsdoc(({ context, indent, jsdoc, settings, utils, // eslint-disable-next-line complexity -- Todo }) => { const { arrayBrackets = 'square', arrowFunctionPostReturnMarkerSpacing = ' ', arrowFunctionPreReturnMarkerSpacing = ' ', enableFixer = true, functionOrClassParameterSpacing = ' ', functionOrClassPostGenericSpacing = '', functionOrClassPostReturnMarkerSpacing = ' ', functionOrClassPreReturnMarkerSpacing = '', functionOrClassTypeParameterSpacing = ' ', genericAndTupleElementSpacing = ' ', genericDot = false, keyValuePostColonSpacing = ' ', keyValuePostKeySpacing = '', keyValuePostOptionalSpacing = '', keyValuePostVariadicSpacing = '', methodQuotes = 'double', objectFieldIndent = '', objectFieldQuote = null, objectFieldSeparator = 'comma', objectFieldSeparatorOptionalLinebreak = true, objectFieldSeparatorTrailingPunctuation = false, parameterDefaultValueSpacing = ' ', postMethodNameSpacing = '', postNewSpacing = ' ', // propertyQuotes = null, separatorForSingleObjectField = false, stringQuotes = 'double', typeBracketSpacing = '', unionSpacing = ' ', } = context.options[0] || {}; const { mode, } = settings; /** * @param {import('@es-joy/jsdoccomment').JsdocTagWithInline} tag */ const checkTypeFormats = (tag) => { const potentialType = tag.type; let parsedType; try { parsedType = mode === 'permissive' ? tryParseType(/** @type {string} */ (potentialType)) : parseType(/** @type {string} */ (potentialType), mode); } catch { return; } const fix = () => { rewireByParsedType(jsdoc, tag, parsedType, indent, typeBracketSpacing); }; /** @type {string[]} */ const errorMessages = []; if (typeBracketSpacing && (!tag.type.startsWith(typeBracketSpacing) || !tag.type.endsWith(typeBracketSpacing))) { errorMessages.push(`Must have initial and final "${typeBracketSpacing}" spacing`); } else if (!typeBracketSpacing && ((/^\s/v).test(tag.type) || (/\s$/v).test(tag.type))) { errorMessages.push('Must have no initial spacing'); } // eslint-disable-next-line complexity -- Todo traverse(parsedType, (nde) => { let errorMessage = ''; /** * @param {Partial<import('jsdoc-type-pratt-parser').FunctionResult['meta']> & { * postNewSpacing?: string, * postMethodNameSpacing?: string * }} meta * @returns {Required<import('jsdoc-type-pratt-parser').FunctionResult['meta']> & { * postNewSpacing?: string, * postMethodNameSpacing?: string * }} */ const conditionalAdds = (meta) => { const typNode = /** * @type {import('jsdoc-type-pratt-parser').FunctionResult| * import('jsdoc-type-pratt-parser').CallSignatureResult| * import('jsdoc-type-pratt-parser').ComputedMethodResult| * import('jsdoc-type-pratt-parser').ConstructorSignatureResult| * import('jsdoc-type-pratt-parser').MethodSignatureResult * } */ (nde); /** * @type {Required<import('jsdoc-type-pratt-parser').FunctionResult['meta']> & { * postNewSpacing?: string, * postMethodNameSpacing?: string * }} */ const newMeta = { parameterSpacing: meta.parameterSpacing ?? typNode.meta?.parameterSpacing ?? ' ', postGenericSpacing: meta.postGenericSpacing ?? typNode.meta?.postGenericSpacing ?? '', postReturnMarkerSpacing: meta.postReturnMarkerSpacing ?? typNode.meta?.postReturnMarkerSpacing ?? ' ', preReturnMarkerSpacing: meta.preReturnMarkerSpacing ?? typNode.meta?.preReturnMarkerSpacing ?? '', typeParameterSpacing: meta.typeParameterSpacing ?? typNode.meta?.typeParameterSpacing ?? ' ', }; if (typNode.type === 'JsdocTypeConstructorSignature') { newMeta.postNewSpacing = meta.postNewSpacing; } if (typNode.type === 'JsdocTypeMethodSignature') { newMeta.postMethodNameSpacing = meta.postMethodNameSpacing ?? typNode.meta?.postMethodNameSpacing ?? ''; } return newMeta; }; switch (nde.type) { case 'JsdocTypeConstructorSignature': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').ConstructorSignatureResult} */ (nde); /* c8 ignore next -- Guard */ if ((typeNode.meta?.postNewSpacing ?? ' ') !== postNewSpacing) { typeNode.meta = /** * @type {Required<import('jsdoc-type-pratt-parser').FunctionResult['meta']> & { * postNewSpacing: string, * }} */ (conditionalAdds({ postNewSpacing, })); errorMessage = `Post-\`new\` spacing should be "${postNewSpacing}"`; break; } } case 'JsdocTypeFunction': { const typeNode = /** * @type {import('jsdoc-type-pratt-parser').FunctionResult} */ nde; if ('arrow' in typeNode && typeNode.arrow) { /* c8 ignore next -- Guard */ if ((typeNode.meta?.postReturnMarkerSpacing ?? ' ') !== arrowFunctionPostReturnMarkerSpacing) { typeNode.meta = /** * @type {Required<import('jsdoc-type-pratt-parser').FunctionResult['meta']> & { * postNewSpacing: string, * }} */ (conditionalAdds({ postReturnMarkerSpacing: arrowFunctionPostReturnMarkerSpacing, /* c8 ignore next -- Guard */ preReturnMarkerSpacing: typeNode.meta?.preReturnMarkerSpacing ?? ' ', })); errorMessage = `Post-return-marker spacing should be "${arrowFunctionPostReturnMarkerSpacing}"`; break; /* c8 ignore next -- Guard */ } else if ((typeNode.meta?.preReturnMarkerSpacing ?? ' ') !== arrowFunctionPreReturnMarkerSpacing) { typeNode.meta = /** * @type {Required<import('jsdoc-type-pratt-parser').FunctionResult['meta']> & { * postNewSpacing: string, * }} */ (conditionalAdds({ /* c8 ignore next -- Guard */ postReturnMarkerSpacing: typeNode.meta?.postReturnMarkerSpacing ?? ' ', preReturnMarkerSpacing: arrowFunctionPreReturnMarkerSpacing, })); errorMessage = `Pre-return-marker spacing should be "${arrowFunctionPreReturnMarkerSpacing}"`; break; } break; } } case 'JsdocTypeCallSignature': case 'JsdocTypeComputedMethod': case 'JsdocTypeMethodSignature': { const typeNode = /** * @type {import('jsdoc-type-pratt-parser').FunctionResult| * import('jsdoc-type-pratt-parser').CallSignatureResult| * import('jsdoc-type-pratt-parser').ComputedMethodResult| * import('jsdoc-type-pratt-parser').ConstructorSignatureResult| * import('jsdoc-type-pratt-parser').MethodSignatureResult * } */ (nde); if (typeNode.type === 'JsdocTypeMethodSignature' && (typeNode.meta?.postMethodNameSpacing ?? '') !== postMethodNameSpacing ) { typeNode.meta = { quote: typeNode.meta.quote, ...conditionalAdds({ postMethodNameSpacing, }), }; errorMessage = `Post-method-name spacing should be "${postMethodNameSpacing}"`; break; } else if (typeNode.type === 'JsdocTypeMethodSignature' && typeNode.meta.quote !== undefined && typeNode.meta.quote !== methodQuotes ) { typeNode.meta = { ...conditionalAdds({ postMethodNameSpacing: typeNode.meta.postMethodNameSpacing ?? '', }), quote: methodQuotes, }; errorMessage = `Method quoting style should be "${methodQuotes}"`; break; } if ((typeNode.meta?.parameterSpacing ?? ' ') !== functionOrClassParameterSpacing) { typeNode.meta = conditionalAdds({ parameterSpacing: functionOrClassParameterSpacing, }); errorMessage = `Parameter spacing should be "${functionOrClassParameterSpacing}"`; } else if ((typeNode.meta?.postGenericSpacing ?? '') !== functionOrClassPostGenericSpacing) { typeNode.meta = conditionalAdds({ postGenericSpacing: functionOrClassPostGenericSpacing, }); errorMessage = `Post-generic spacing should be "${functionOrClassPostGenericSpacing}"`; } else if ((typeNode.meta?.postReturnMarkerSpacing ?? ' ') !== functionOrClassPostReturnMarkerSpacing) { typeNode.meta = conditionalAdds({ postReturnMarkerSpacing: functionOrClassPostReturnMarkerSpacing, }); errorMessage = `Post-return-marker spacing should be "${functionOrClassPostReturnMarkerSpacing}"`; } else if ((typeNode.meta?.preReturnMarkerSpacing ?? '') !== functionOrClassPreReturnMarkerSpacing) { typeNode.meta = conditionalAdds({ preReturnMarkerSpacing: functionOrClassPreReturnMarkerSpacing, }); errorMessage = `Pre-return-marker spacing should be "${functionOrClassPreReturnMarkerSpacing}"`; } else if ((typeNode.meta?.typeParameterSpacing ?? ' ') !== functionOrClassTypeParameterSpacing) { typeNode.meta = conditionalAdds({ typeParameterSpacing: functionOrClassTypeParameterSpacing, }); errorMessage = `Type parameter spacing should be "${functionOrClassTypeParameterSpacing}"`; } break; } case 'JsdocTypeGeneric': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ (nde); if ('value' in typeNode.left && typeNode.left.value === 'Array') { if (typeNode.meta.brackets !== arrayBrackets) { typeNode.meta.brackets = arrayBrackets; errorMessage = `Array bracket style should be ${arrayBrackets}`; } } else if (typeNode.meta.dot !== genericDot) { typeNode.meta.dot = genericDot; errorMessage = `Dot usage should be ${genericDot}`; } else if ((typeNode.meta.elementSpacing ?? ' ') !== genericAndTupleElementSpacing) { typeNode.meta.elementSpacing = genericAndTupleElementSpacing; errorMessage = `Element spacing should be "${genericAndTupleElementSpacing}"`; } break; } case 'JsdocTypeKeyValue': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').KeyValueResult} */ (nde); /* c8 ignore next -- Guard */ if ((typeNode.meta?.postKeySpacing ?? '') !== keyValuePostKeySpacing) { typeNode.meta = { /* c8 ignore next -- Guard */ postColonSpacing: typeNode.meta?.postColonSpacing ?? ' ', postKeySpacing: keyValuePostKeySpacing, /* c8 ignore next 2 -- Guard */ postOptionalSpacing: typeNode.meta?.postOptionalSpacing ?? '', postVariadicSpacing: typeNode.meta?.postVariadicSpacing ?? '', }; errorMessage = `Post key spacing should be "${keyValuePostKeySpacing}"`; /* c8 ignore next -- Guard */ } else if ((typeNode.meta?.postColonSpacing ?? ' ') !== keyValuePostColonSpacing) { typeNode.meta = { postColonSpacing: keyValuePostColonSpacing, /* c8 ignore next 3 -- Guard */ postKeySpacing: typeNode.meta?.postKeySpacing ?? '', postOptionalSpacing: typeNode.meta?.postOptionalSpacing ?? '', postVariadicSpacing: typeNode.meta?.postVariadicSpacing ?? '', }; errorMessage = `Post colon spacing should be "${keyValuePostColonSpacing}"`; /* c8 ignore next -- Guard */ } else if ((typeNode.meta?.postOptionalSpacing ?? '') !== keyValuePostOptionalSpacing) { typeNode.meta = { /* c8 ignore next 2 -- Guard */ postColonSpacing: typeNode.meta?.postColonSpacing ?? ' ', postKeySpacing: typeNode.meta?.postKeySpacing ?? '', postOptionalSpacing: keyValuePostOptionalSpacing, /* c8 ignore next -- Guard */ postVariadicSpacing: typeNode.meta?.postVariadicSpacing ?? '', }; errorMessage = `Post optional (\`?\`) spacing should be "${keyValuePostOptionalSpacing}"`; /* c8 ignore next -- Guard */ } else if (typeNode.variadic && (typeNode.meta?.postVariadicSpacing ?? '') !== keyValuePostVariadicSpacing) { typeNode.meta = { /* c8 ignore next 3 -- Guard */ postColonSpacing: typeNode.meta?.postColonSpacing ?? ' ', postKeySpacing: typeNode.meta?.postKeySpacing ?? '', postOptionalSpacing: typeNode.meta?.postOptionalSpacing ?? '', postVariadicSpacing: keyValuePostVariadicSpacing, }; errorMessage = `Post variadic (\`...\`) spacing should be "${keyValuePostVariadicSpacing}"`; } break; } case 'JsdocTypeObject': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').ObjectResult} */ (nde); /* c8 ignore next -- Guard */ const separator = typeNode.meta.separator ?? 'comma'; if ( (separator !== objectFieldSeparator && (!objectFieldSeparatorOptionalLinebreak || !(objectFieldSeparator.endsWith('-linebreak') && objectFieldSeparator.startsWith(separator)))) || (typeNode.meta.separatorForSingleObjectField ?? false) !== separatorForSingleObjectField || ((typeNode.meta.propertyIndent ?? '') !== objectFieldIndent && separator.endsWith('-linebreak')) || (typeNode.meta.trailingPunctuation ?? false) !== objectFieldSeparatorTrailingPunctuation ) { typeNode.meta.separator = objectFieldSeparatorOptionalLinebreak && !separator.endsWith('and-linebreak') ? objectFieldSeparator.replace(/-and-linebreak$/v, '') : objectFieldSeparator; typeNode.meta.separatorForSingleObjectField = separatorForSingleObjectField; typeNode.meta.propertyIndent = objectFieldIndent; typeNode.meta.trailingPunctuation = objectFieldSeparatorTrailingPunctuation; errorMessage = `Inconsistent ${objectFieldSeparator} separator usage`; } break; } case 'JsdocTypeObjectField': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').ObjectFieldResult} */ (nde); if ((objectFieldQuote || (typeof typeNode.key === 'string' && ( (/^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/v).test(typeNode.key) || digitRegex.test(typeNode.key) ) )) && typeNode.meta.quote !== (objectFieldQuote ?? undefined) && (typeof typeNode.key !== 'string' || !digitRegex.test(typeNode.key)) ) { typeNode.meta.quote = objectFieldQuote ?? undefined; errorMessage = `Inconsistent object field quotes ${objectFieldQuote}`; } else if ((typeNode.meta?.postKeySpacing ?? '') !== keyValuePostKeySpacing) { typeNode.meta.postKeySpacing = keyValuePostKeySpacing; errorMessage = `Post key spacing should be "${keyValuePostKeySpacing}"`; } else if ((typeNode.meta?.postColonSpacing ?? ' ') !== keyValuePostColonSpacing) { typeNode.meta.postColonSpacing = keyValuePostColonSpacing; errorMessage = `Post colon spacing should be "${keyValuePostColonSpacing}"`; } else if ((typeNode.meta?.postOptionalSpacing ?? '') !== keyValuePostOptionalSpacing) { typeNode.meta.postOptionalSpacing = keyValuePostOptionalSpacing; errorMessage = `Post optional (\`?\`) spacing should be "${keyValuePostOptionalSpacing}"`; } break; } case 'JsdocTypeStringValue': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').StringValueResult} */ (nde); if (typeNode.meta.quote !== stringQuotes) { typeNode.meta.quote = stringQuotes; errorMessage = `Inconsistent ${stringQuotes} string quotes usage`; } break; } // Only suitable for namepaths (and would need changes); see https://github.com/gajus/eslint-plugin-jsdoc/issues/1524 // case 'JsdocTypeProperty': { // const typeNode = /** @type {import('jsdoc-type-pratt-parser').PropertyResult} */ (nde); // if ((propertyQuotes || // (typeof typeNode.value === 'string' && !(/\s/v).test(typeNode.value))) && // typeNode.meta.quote !== (propertyQuotes ?? undefined) // ) { // typeNode.meta.quote = propertyQuotes ?? undefined; // errorMessage = `Inconsistent ${propertyQuotes} property quotes usage`; // } // break; // } case 'JsdocTypeTuple': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').TupleResult} */ (nde); /* c8 ignore next -- Guard */ if ((typeNode.meta?.elementSpacing ?? ' ') !== genericAndTupleElementSpacing) { typeNode.meta = { elementSpacing: genericAndTupleElementSpacing, }; errorMessage = `Element spacing should be "${genericAndTupleElementSpacing}"`; } break; } case 'JsdocTypeTypeParameter': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').TypeParameterResult} */ (nde); /* c8 ignore next -- Guard */ if (typeNode.defaultValue && (typeNode.meta?.defaultValueSpacing ?? ' ') !== parameterDefaultValueSpacing) { typeNode.meta = { defaultValueSpacing: parameterDefaultValueSpacing, }; errorMessage = `Default value spacing should be "${parameterDefaultValueSpacing}"`; } break; } case 'JsdocTypeUnion': { const typeNode = /** @type {import('jsdoc-type-pratt-parser').UnionResult} */ (nde); /* c8 ignore next -- Guard */ if ((typeNode.meta?.spacing ?? ' ') !== unionSpacing) { typeNode.meta = { spacing: unionSpacing, }; errorMessage = `Inconsistent "${unionSpacing}" union spacing usage`; } break; } default: break; } if (errorMessage) { errorMessages.push(errorMessage); } }); const differentResult = tag.type !== typeBracketSpacing + stringify(parsedType) + typeBracketSpacing; if (errorMessages.length && differentResult) { for (const errorMessage of errorMessages) { utils.reportJSDoc( errorMessage, tag, enableFixer ? fix : null, ); } // Stringification may have been equal previously (and thus no error reported) // because the stringification doesn't preserve everything } else if (differentResult) { utils.reportJSDoc( 'There was an error with type formatting', tag, enableFixer ? fix : null, ); } }; const tags = utils.getPresentTags([ 'param', 'property', 'returns', 'this', 'throws', 'type', 'typedef', 'yields', ]); for (const tag of tags) { if (tag.type) { checkTypeFormats(tag); } } }, { iterateAllJsdocs: true, meta: { docs: { description: 'Formats JSDoc type values.', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/type-formatting.md#repos-sticky-header', }, fixable: 'code', schema: [ { additionalProperties: false, properties: { arrayBrackets: { description: 'Determines how array generics are represented. Set to `angle` for the style `Array<type>` or `square` for the style `type[]`. Defaults to "square".', enum: [ 'angle', 'square', ], type: 'string', }, arrowFunctionPostReturnMarkerSpacing: { description: 'The space character (if any) to use after return markers (`=>`). Defaults to " ".', type: 'string', }, arrowFunctionPreReturnMarkerSpacing: { description: 'The space character (if any) to use before return markers (`=>`). Defaults to " ".', type: 'string', }, enableFixer: { description: 'Whether to enable the fixer. Defaults to `true`.', type: 'boolean', }, functionOrClassParameterSpacing: { description: 'The space character (if any) to use between function or class parameters. Defaults to " ".', type: 'string', }, functionOrClassPostGenericSpacing: { description: 'The space character (if any) to use after a generic expression in a function or class. Defaults to "".', type: 'string', }, functionOrClassPostReturnMarkerSpacing: { description: 'The space character (if any) to use after return markers (`:`). Defaults to "".', type: 'string', }, functionOrClassPreReturnMarkerSpacing: { description: 'The space character (if any) to use before return markers (`:`). Defaults to "".', type: 'string', }, functionOrClassTypeParameterSpacing: { description: 'The space character (if any) to use between type parameters in a function or class. Defaults to " ".', type: 'string', }, genericAndTupleElementSpacing: { description: 'The space character (if any) to use between elements in generics and tuples. Defaults to " ".', type: 'string', }, genericDot: { description: 'Boolean value of whether to use a dot before the angled brackets of a generic (e.g., `SomeType.<AnotherType>`). Defaults to `false`.', type: 'boolean', }, keyValuePostColonSpacing: { description: 'The amount of spacing (if any) after the colon of a key-value or object-field pair. Defaults to " ".', type: 'string', }, keyValuePostKeySpacing: { description: 'The amount of spacing (if any) immediately after keys in a key-value or object-field pair. Defaults to "".', type: 'string', }, keyValuePostOptionalSpacing: { description: 'The amount of spacing (if any) after the optional operator (`?`) in a key-value or object-field pair. Defaults to "".', type: 'string', }, keyValuePostVariadicSpacing: { description: 'The amount of spacing (if any) after a variadic operator (`...`) in a key-value pair. Defaults to "".', type: 'string', }, methodQuotes: { description: 'The style of quotation mark for surrounding method names when quoted. Defaults to `double`', enum: [ 'double', 'single', ], type: 'string', }, objectFieldIndent: { description: `A string indicating the whitespace to be added on each line preceding an object property-value field. Defaults to the empty string.`, type: 'string', }, objectFieldQuote: { description: `Whether and how object field properties should be quoted (e.g., \`{"a": string}\`). Set to \`single\`, \`double\`, or \`null\`. Defaults to \`null\` (no quotes unless required due to special characters within the field). Digits will be kept as is, regardless of setting (they can either represent a digit or a string digit).`, enum: [ 'double', 'single', null, ], }, objectFieldSeparator: { description: `For object properties, specify whether a "semicolon", "comma", "linebreak", "semicolon-and-linebreak", or "comma-and-linebreak" should be used after each object property-value pair. Defaults to \`"comma"\`.`, enum: [ 'comma', 'comma-and-linebreak', 'linebreak', 'semicolon', 'semicolon-and-linebreak', ], type: 'string', }, objectFieldSeparatorOptionalLinebreak: { description: `Whether \`objectFieldSeparator\` set to \`"semicolon-and-linebreak"\` or \`"comma-and-linebreak"\` should be allowed to optionally drop the linebreak. Defaults to \`true\`.`, type: 'boolean', }, objectFieldSeparatorTrailingPunctuation: { description: `If \`separatorForSingleObjectField\` is not in effect (i.e., if it is \`false\` or there are multiple property-value object fields present), this property will determine whether to add punctuation corresponding to the \`objectFieldSeparator\` (e.g., a semicolon) to the final object field. Defaults to \`false\`.`, type: 'boolean', }, parameterDefaultValueSpacing: { description: 'The space character (if any) to use between the equal signs of a default value. Defaults to " ".', type: 'string', }, postMethodNameSpacing: { description: 'The space character (if any) to add after a method name. Defaults to "".', type: 'string', }, postNewSpacing: { description: 'The space character (if any) to add after "new" in a constructor. Defaults to " ".', type: 'string', }, // propertyQuotes: { // description: `Whether and how namepath properties should be quoted (e.g., \`ab."cd"."ef"\`). // Set to \`single\`, \`double\`, or \`null\`. Defaults to \`null\` (no quotes unless // required due to whitespace within the property).`, // enum: [ // 'double', // 'single', // null, // ], // }, separatorForSingleObjectField: { description: `Whether to apply the \`objectFieldSeparator\` (e.g., a semicolon) when there is only one property-value object field present. Defaults to \`false\`.`, type: 'boolean', }, stringQuotes: { description: `How string literals should be quoted (e.g., \`"abc"\`). Set to \`single\` or \`double\`. Defaults to 'double'.`, enum: [ 'double', 'single', ], type: 'string', }, typeBracketSpacing: { description: `A string of spaces that will be added immediately after the type's initial curly bracket and immediately before its ending curly bracket. Defaults to the empty string.`, type: 'string', }, unionSpacing: { description: 'Determines the spacing to add to unions (`|`). Defaults to a single space (`" "`).', type: 'string', }, }, type: 'object', }, ], type: 'suggestion', }, });