blob: 6583cdc204194ed60c6129b9634083554f063c98 [file] [log] [blame]
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _iterateJsdoc = _interopRequireDefault(require("../iterateJsdoc.cjs"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
* @typedef {[string, boolean, () => RootNamerReturn]} RootNamerReturn
*/
/**
* @param {string[]} desiredRoots
* @param {number} currentIndex
* @returns {RootNamerReturn}
*/
const rootNamer = (desiredRoots, currentIndex) => {
/** @type {string} */
let name;
let idx = currentIndex;
const incremented = desiredRoots.length <= 1;
if (incremented) {
const base = desiredRoots[0];
const suffix = idx++;
name = `${base}${suffix}`;
} else {
name = /** @type {string} */desiredRoots.shift();
}
return [name, incremented, () => {
return rootNamer(desiredRoots, idx);
}];
};
/* eslint-disable complexity -- Temporary */
var _default = exports.default = (0, _iterateJsdoc.default)(({
context,
jsdoc,
node,
utils
}) => {
/* eslint-enable complexity -- Temporary */
if (utils.avoidDocs()) {
return;
}
// Param type is specified by type in @type
if (utils.hasTag('type')) {
return;
}
const {
autoIncrementBase = 0,
checkDestructured = true,
checkDestructuredRoots = true,
checkRestProperty = false,
checkTypesPattern = '/^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$/',
enableFixer = true,
enableRestElementFixer = true,
enableRootFixer = true,
ignoreWhenAllParamsMissing = false,
interfaceExemptsParamsCheck = false,
unnamedRootBase = ['root'],
useDefaultObjectProperties = false
} = context.options[0] || {};
if (interfaceExemptsParamsCheck) {
if (node && 'params' in node && node.params.length === 1 && node.params?.[0] && typeof node.params[0] === 'object' && node.params[0].type === 'ObjectPattern' && 'typeAnnotation' in node.params[0] && node.params[0].typeAnnotation) {
return;
}
if (node && node.parent.type === 'VariableDeclarator' && 'typeAnnotation' in node.parent.id && node.parent.id.typeAnnotation) {
return;
}
}
const preferredTagName = /** @type {string} */utils.getPreferredTagName({
tagName: 'param'
});
if (!preferredTagName) {
return;
}
const functionParameterNames = utils.getFunctionParameterNames(useDefaultObjectProperties);
if (!functionParameterNames.length) {
return;
}
const jsdocParameterNames =
/**
* @type {{
* idx: import('../iterateJsdoc.js').Integer;
* name: string;
* type: string;
* }[]}
*/
utils.getJsdocTagsDeep(preferredTagName);
if (ignoreWhenAllParamsMissing && !jsdocParameterNames.length) {
return;
}
const shallowJsdocParameterNames = jsdocParameterNames.filter(tag => {
return !tag.name.includes('.');
}).map((tag, idx) => {
return {
...tag,
idx
};
});
const checkTypesRegex = utils.getRegexFromString(checkTypesPattern);
/**
* @type {{
* functionParameterIdx: import('../iterateJsdoc.js').Integer,
* functionParameterName: string,
* inc: boolean|undefined,
* remove?: true,
* type?: string|undefined
* }[]}
*/
const missingTags = [];
const flattenedRoots = utils.flattenRoots(functionParameterNames).names;
/**
* @type {{
* [key: string]: import('../iterateJsdoc.js').Integer
* }}
*/
const paramIndex = {};
/**
* @param {string} cur
* @returns {boolean}
*/
const hasParamIndex = cur => {
return utils.dropPathSegmentQuotes(String(cur)) in paramIndex;
};
/**
*
* @param {string|number|undefined} cur
* @returns {import('../iterateJsdoc.js').Integer}
*/
const getParamIndex = cur => {
return paramIndex[utils.dropPathSegmentQuotes(String(cur))];
};
/**
*
* @param {string} cur
* @param {import('../iterateJsdoc.js').Integer} idx
* @returns {void}
*/
const setParamIndex = (cur, idx) => {
paramIndex[utils.dropPathSegmentQuotes(String(cur))] = idx;
};
for (const [idx, cur] of flattenedRoots.entries()) {
setParamIndex(cur, idx);
}
/**
*
* @param {(import('@es-joy/jsdoccomment').JsdocTagWithInline & {
* newAdd?: boolean
* })[]} jsdocTags
* @param {import('../iterateJsdoc.js').Integer} indexAtFunctionParams
* @returns {{
* foundIndex: import('../iterateJsdoc.js').Integer,
* tagLineCount: import('../iterateJsdoc.js').Integer,
* }}
*/
const findExpectedIndex = (jsdocTags, indexAtFunctionParams) => {
const remainingRoots = functionParameterNames.slice(indexAtFunctionParams || 0);
const foundIndex = jsdocTags.findIndex(({
name,
newAdd
}) => {
return !newAdd && remainingRoots.some(remainingRoot => {
if (Array.isArray(remainingRoot)) {
return (
/**
* @type {import('../jsdocUtils.js').FlattendRootInfo & {
* annotationParamName?: string|undefined;
* }}
*/
remainingRoot[1].names.includes(name)
);
}
if (typeof remainingRoot === 'object') {
return name === remainingRoot.name;
}
return name === remainingRoot;
});
});
const tags = foundIndex > -1 ? jsdocTags.slice(0, foundIndex) : jsdocTags.filter(({
tag
}) => {
return tag === preferredTagName;
});
let tagLineCount = 0;
for (const {
source
} of tags) {
for (const {
tokens: {
end
}
} of source) {
if (!end) {
tagLineCount++;
}
}
}
return {
foundIndex,
tagLineCount
};
};
let [nextRootName, incremented, namer] = rootNamer([...unnamedRootBase], autoIncrementBase);
const thisOffset = functionParameterNames[0] === 'this' ? 1 : 0;
for (const [functionParameterIdx, functionParameterName] of functionParameterNames.entries()) {
let inc;
if (Array.isArray(functionParameterName)) {
const matchedJsdoc = shallowJsdocParameterNames[functionParameterIdx - thisOffset];
/** @type {string} */
let rootName;
if (functionParameterName[0]) {
rootName = functionParameterName[0];
} else if (matchedJsdoc && matchedJsdoc.name) {
rootName = matchedJsdoc.name;
if (matchedJsdoc.type && matchedJsdoc.type.search(checkTypesRegex) === -1) {
continue;
}
} else {
rootName = nextRootName;
inc = incremented;
}
[nextRootName, incremented, namer] = namer();
const {
hasPropertyRest,
hasRestElement,
names,
rests
} =
/**
* @type {import('../jsdocUtils.js').FlattendRootInfo & {
* annotationParamName?: string | undefined;
* }}
*/
functionParameterName[1];
const notCheckingNames = [];
if (!enableRestElementFixer && hasRestElement) {
continue;
}
if (!checkDestructuredRoots) {
continue;
}
for (const [idx, paramName] of names.entries()) {
// Add root if the root name is not in the docs (and is not already
// in the tags to be fixed)
if (!jsdocParameterNames.find(({
name
}) => {
return name === rootName;
}) && !missingTags.find(({
functionParameterName: fpn
}) => {
return fpn === rootName;
})) {
const emptyParamIdx = jsdocParameterNames.findIndex(({
name
}) => {
return !name;
});
if (emptyParamIdx > -1) {
missingTags.push({
functionParameterIdx: emptyParamIdx,
functionParameterName: rootName,
inc,
remove: true
});
} else {
missingTags.push({
functionParameterIdx: hasParamIndex(rootName) ? getParamIndex(rootName) : getParamIndex(paramName),
functionParameterName: rootName,
inc
});
}
}
if (!checkDestructured) {
continue;
}
if (!checkRestProperty && rests[idx]) {
continue;
}
const fullParamName = `${rootName}.${paramName}`;
const notCheckingName = jsdocParameterNames.find(({
name,
type: paramType
}) => {
return utils.comparePaths(name)(fullParamName) && paramType.search(checkTypesRegex) === -1 && paramType !== '';
});
if (notCheckingName !== undefined) {
notCheckingNames.push(notCheckingName.name);
}
if (notCheckingNames.find(name => {
return fullParamName.startsWith(name);
})) {
continue;
}
if (jsdocParameterNames && !jsdocParameterNames.find(({
name
}) => {
return utils.comparePaths(name)(fullParamName);
})) {
missingTags.push({
functionParameterIdx: getParamIndex(functionParameterName[0] ? fullParamName : paramName),
functionParameterName: fullParamName,
inc,
type: hasRestElement && !hasPropertyRest ? '{...any}' : undefined
});
}
}
continue;
}
/** @type {string} */
let funcParamName;
let type;
if (typeof functionParameterName === 'object') {
if (!enableRestElementFixer && functionParameterName.restElement) {
continue;
}
funcParamName = /** @type {string} */functionParameterName.name;
type = '{...any}';
} else {
funcParamName = /** @type {string} */functionParameterName;
}
if (jsdocParameterNames && !jsdocParameterNames.find(({
name
}) => {
return name === funcParamName;
}) && funcParamName !== 'this') {
missingTags.push({
functionParameterIdx: getParamIndex(funcParamName),
functionParameterName: funcParamName,
inc,
type
});
}
}
/**
*
* @param {{
* functionParameterIdx: import('../iterateJsdoc.js').Integer,
* functionParameterName: string,
* remove?: true,
* inc?: boolean,
* type?: string
* }} cfg
*/
const fix = ({
functionParameterIdx,
functionParameterName,
inc,
remove,
type
}) => {
if (inc && !enableRootFixer) {
return;
}
/**
*
* @param {import('../iterateJsdoc.js').Integer} tagIndex
* @param {import('../iterateJsdoc.js').Integer} sourceIndex
* @param {import('../iterateJsdoc.js').Integer} spliceCount
* @returns {void}
*/
const createTokens = (tagIndex, sourceIndex, spliceCount) => {
// console.log(sourceIndex, tagIndex, jsdoc.tags, jsdoc.source);
const tokens = {
number: sourceIndex + 1,
source: '',
tokens: {
delimiter: '*',
description: '',
end: '',
lineEnd: '',
name: functionParameterName,
newAdd: true,
postDelimiter: ' ',
postName: '',
postTag: ' ',
postType: type ? ' ' : '',
start: jsdoc.source[sourceIndex].tokens.start,
tag: `@${preferredTagName}`,
type: type ?? ''
}
};
/**
* @type {(import('@es-joy/jsdoccomment').JsdocTagWithInline & {
* newAdd?: true
* })[]}
*/
jsdoc.tags.splice(tagIndex, spliceCount, {
description: '',
inlineTags: [],
name: functionParameterName,
newAdd: true,
optional: false,
problems: [],
source: [tokens],
tag: preferredTagName,
type: type ?? ''
});
const firstNumber = jsdoc.source[0].number;
jsdoc.source.splice(sourceIndex, spliceCount, tokens);
for (const [idx, src] of jsdoc.source.slice(sourceIndex).entries()) {
src.number = firstNumber + sourceIndex + idx;
}
};
const offset = jsdoc.source.findIndex(({
tokens: {
end,
tag
}
}) => {
return tag || end;
});
if (remove) {
createTokens(functionParameterIdx, offset + functionParameterIdx, 1);
} else {
const {
foundIndex,
tagLineCount: expectedIdx
} = findExpectedIndex(jsdoc.tags, functionParameterIdx);
const firstParamLine = jsdoc.source.findIndex(({
tokens
}) => {
return tokens.tag === `@${preferredTagName}`;
});
const baseOffset = foundIndex > -1 || firstParamLine === -1 ? offset : firstParamLine;
createTokens(expectedIdx, baseOffset + expectedIdx, 0);
}
};
/**
* @returns {void}
*/
const fixer = () => {
for (const missingTag of missingTags) {
fix(missingTag);
}
};
if (missingTags.length && jsdoc.source.length === 1) {
utils.makeMultiline();
}
for (const {
functionParameterName
} of missingTags) {
utils.reportJSDoc(`Missing JSDoc @${preferredTagName} "${functionParameterName}" declaration.`, null, enableFixer ? fixer : null);
}
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that all function parameters are documented with a `@param` tag.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-param.md#repos-sticky-header'
},
fixable: 'code',
schema: [{
additionalProperties: false,
properties: {
autoIncrementBase: {
default: 0,
description: `Numeric to indicate the number at which to begin auto-incrementing roots.
Defaults to \`0\`.
`,
type: 'integer'
},
checkConstructors: {
default: true,
description: `A value indicating whether \`constructor\`s should be checked. Defaults to
\`true\`.
`,
type: 'boolean'
},
checkDestructured: {
default: true,
description: 'Whether to require destructured properties. Defaults to `true`.',
type: 'boolean'
},
checkDestructuredRoots: {
default: true,
description: `Whether to check the existence of a corresponding \`@param\` for root objects
of destructured properties (e.g., that for \`function ({a, b}) {}\`, that there
is something like \`@param myRootObj\` defined that can correspond to
the \`{a, b}\` object parameter).
If \`checkDestructuredRoots\` is \`false\`, \`checkDestructured\` will also be
implied to be \`false\` (i.e., the inside of the roots will not be checked
either, e.g., it will also not complain if \`a\` or \`b\` do not have their own
documentation). Defaults to \`true\`.
`,
type: 'boolean'
},
checkGetters: {
default: false,
description: 'A value indicating whether getters should be checked. Defaults to `false`.',
type: 'boolean'
},
checkRestProperty: {
default: false,
description: `If set to \`true\`, will report (and add fixer insertions) for missing rest
properties. Defaults to \`false\`.
If set to \`true\`, note that you can still document the subproperties of the
rest property using other jsdoc features, e.g., \`@typedef\`:
\`\`\`js
/**
* @typedef ExtraOptions
* @property innerProp1
* @property innerProp2
*/
/**
* @param cfg
* @param cfg.num
* @param {ExtraOptions} extra
*/
function quux ({num, ...extra}) {
}
\`\`\`
Setting this option to \`false\` (the default) may be useful in cases where
you already have separate \`@param\` definitions for each of the properties
within the rest property.
For example, with the option disabled, this will not give an error despite
\`extra\` not having any definition:
\`\`\`js
/**
* @param cfg
* @param cfg.num
*/
function quux ({num, ...extra}) {
}
\`\`\`
Nor will this:
\`\`\`js
/**
* @param cfg
* @param cfg.num
* @param cfg.innerProp1
* @param cfg.innerProp2
*/
function quux ({num, ...extra}) {
}
\`\`\`
`,
type: 'boolean'
},
checkSetters: {
default: false,
description: 'A value indicating whether setters should be checked. Defaults to `false`.',
type: 'boolean'
},
checkTypesPattern: {
description: `When one specifies a type, unless it is of a generic type, like \`object\`
or \`array\`, it may be considered unnecessary to have that object's
destructured components required, especially where generated docs will
link back to the specified type. For example:
\`\`\`js
/**
* @param {SVGRect} bbox - a SVGRect
*/
export const bboxToObj = function ({x, y, width, height}) {
return {x, y, width, height};
};
\`\`\`
By default \`checkTypesPattern\` is set to
\`/^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$/v\`,
meaning that destructuring will be required only if the type of the \`@param\`
(the text between curly brackets) is a match for "Object" or "Array" (with or
without initial caps), "PlainObject", or "GenericObject", "GenericArray" (or
if no type is present). So in the above example, the lack of a match will
mean that no complaint will be given about the undocumented destructured
parameters.
Note that the \`/\` delimiters are optional, but necessary to add flags.
Defaults to using (only) the \`v\` flag, so to add your own flags, encapsulate
your expression as a string, but like a literal, e.g., \`/^object$/vi\`.
You could set this regular expression to a more expansive list, or you
could restrict it such that even types matching those strings would not
need destructuring.`,
type: 'string'
},
contexts: {
description: `Set this to an array of strings representing the AST context (or an object with
optional \`context\` and \`comment\` properties) where you wish the rule to be applied.
\`context\` defaults to \`any\` and \`comment\` defaults to no specific comment context.
Overrides the default contexts (\`ArrowFunctionExpression\`, \`FunctionDeclaration\`,
\`FunctionExpression\`). May be useful for adding such as
\`TSMethodSignature\` in TypeScript or restricting the contexts
which are checked.
See the ["AST and Selectors"](../#advanced-ast-and-selectors)
section of our Advanced docs for more on the expected format.
`,
items: {
anyOf: [{
type: 'string'
}, {
additionalProperties: false,
properties: {
comment: {
type: 'string'
},
context: {
type: 'string'
}
},
type: 'object'
}]
},
type: 'array'
},
enableFixer: {
description: 'Whether to enable the fixer. Defaults to `true`.',
type: 'boolean'
},
enableRestElementFixer: {
description: `Whether to enable the rest element fixer.
The fixer will automatically report/insert
[JSDoc repeatable parameters](https://jsdoc.app/tags-param.html#multiple-types-and-repeatable-parameters)
if missing.
\`\`\`js
/**
* @param {GenericArray} cfg
* @param {number} cfg."0"
*/
function baar ([a, ...extra]) {
//
}
\`\`\`
...becomes:
\`\`\`js
/**
* @param {GenericArray} cfg
* @param {number} cfg."0"
* @param {...any} cfg."1"
*/
function baar ([a, ...extra]) {
//
}
\`\`\`
Note that the type \`any\` is included since we don't know of any specific
type to use.
Defaults to \`true\`.
`,
type: 'boolean'
},
enableRootFixer: {
description: `Whether to enable the auto-adding of incrementing roots.
The default behavior of \`true\` is for "root" to be auto-inserted for missing
roots, followed by a 0-based auto-incrementing number.
So for:
\`\`\`js
function quux ({foo}, {bar}, {baz}) {
}
\`\`\`
...the default JSDoc that would be added if the fixer is enabled would be:
\`\`\`js
/**
* @param root0
* @param root0.foo
* @param root1
* @param root1.bar
* @param root2
* @param root2.baz
*/
\`\`\`
Has no effect if \`enableFixer\` is set to \`false\`.`,
type: 'boolean'
},
exemptedBy: {
description: `Array of tags (e.g., \`['type']\`) whose presence on the document block
avoids the need for a \`@param\`. Defaults to an array with
\`inheritdoc\`. If you set this array, it will overwrite the default,
so be sure to add back \`inheritdoc\` if you wish its presence to cause
exemption of the rule.`,
items: {
type: 'string'
},
type: 'array'
},
ignoreWhenAllParamsMissing: {
description: `Set to \`true\` to ignore reporting when all params are missing. Defaults to
\`false\`.`,
type: 'boolean'
},
interfaceExemptsParamsCheck: {
description: `Set if you wish TypeScript interfaces to exempt checks for the existence of
\`@param\`'s.
Will check for a type defining the function itself (on a variable
declaration) or if there is a single destructured object with a type.
Defaults to \`false\`.`,
type: 'boolean'
},
unnamedRootBase: {
description: `An array of root names to use in the fixer when roots are missing. Defaults
to \`['root']\`. Note that only when all items in the array besides the last
are exhausted will auto-incrementing occur. So, with
\`unnamedRootBase: ['arg', 'config']\`, the following:
\`\`\`js
function quux ({foo}, [bar], {baz}) {
}
\`\`\`
...will get the following JSDoc block added:
\`\`\`js
/**
* @param arg
* @param arg.foo
* @param config0
* @param config0."0" (\`bar\`)
* @param config1
* @param config1.baz
*/
\`\`\``,
items: {
type: 'string'
},
type: 'array'
},
useDefaultObjectProperties: {
description: `Set to \`true\` if you wish to expect documentation of properties on objects
supplied as default values. Defaults to \`false\`.`,
type: 'boolean'
}
},
type: 'object'
}],
type: 'suggestion'
},
// We cannot cache comment nodes as the contexts may recur with the
// same comment node but a different JS node, and we may need the different
// JS node to ensure we iterate its context
noTracking: true
});
module.exports = exports.default;
//# sourceMappingURL=requireParam.cjs.map