1881 lines
49 KiB
JavaScript
1881 lines
49 KiB
JavaScript
import getDefaultTagStructureForMode from './getDefaultTagStructureForMode.js';
|
|
import {
|
|
closureTags,
|
|
jsdocTags,
|
|
typeScriptTags,
|
|
} from './tagNames.js';
|
|
import {
|
|
hasReturnValue,
|
|
hasValueOrExecutorHasNonEmptyResolveValue,
|
|
} from './utils/hasReturnValue.js';
|
|
import WarnSettings from './WarnSettings.js';
|
|
import {
|
|
tryParse,
|
|
} from '@es-joy/jsdoccomment';
|
|
|
|
/**
|
|
* @typedef {number} Integer
|
|
*/
|
|
/**
|
|
* @typedef {import('./utils/hasReturnValue.js').ESTreeOrTypeScriptNode} ESTreeOrTypeScriptNode
|
|
*/
|
|
|
|
/**
|
|
* @typedef {"jsdoc"|"typescript"|"closure"|"permissive"} ParserMode
|
|
*/
|
|
|
|
/**
|
|
* @type {import('./getDefaultTagStructureForMode.js').TagStructure}
|
|
*/
|
|
let tagStructure;
|
|
|
|
/**
|
|
* @param {ParserMode} mode
|
|
* @returns {void}
|
|
*/
|
|
const setTagStructure = (mode) => {
|
|
tagStructure = getDefaultTagStructureForMode(mode);
|
|
};
|
|
|
|
/**
|
|
* @typedef {undefined|string|{
|
|
* name: Integer,
|
|
* restElement: boolean
|
|
* }|{
|
|
* isRestProperty: boolean|undefined,
|
|
* name: string,
|
|
* restElement: boolean
|
|
* }|{
|
|
* name: string,
|
|
* restElement: boolean
|
|
* }} ParamCommon
|
|
*/
|
|
/**
|
|
* @typedef {ParamCommon|[string|undefined, (FlattendRootInfo & {
|
|
* annotationParamName?: string,
|
|
* })]|NestedParamInfo} ParamNameInfo
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* hasPropertyRest: boolean,
|
|
* hasRestElement: boolean,
|
|
* names: string[],
|
|
* rests: boolean[],
|
|
* }} FlattendRootInfo
|
|
*/
|
|
/**
|
|
* @typedef {[string, (string[]|ParamInfo[])]} NestedParamInfo
|
|
*/
|
|
/**
|
|
* @typedef {ParamCommon|
|
|
* [string|undefined, (FlattendRootInfo & {
|
|
* annotationParamName?: string
|
|
* })]|
|
|
* NestedParamInfo} ParamInfo
|
|
*/
|
|
|
|
/**
|
|
* Given a nested array of property names, reduce them to a single array,
|
|
* appending the name of the root element along the way if present.
|
|
* @callback FlattenRoots
|
|
* @param {ParamInfo[]} params
|
|
* @param {string} [root]
|
|
* @returns {FlattendRootInfo}
|
|
*/
|
|
|
|
/** @type {FlattenRoots} */
|
|
const flattenRoots = (params, root = '') => {
|
|
let hasRestElement = false;
|
|
let hasPropertyRest = false;
|
|
|
|
/**
|
|
* @type {boolean[]}
|
|
*/
|
|
const rests = [];
|
|
|
|
const names = params.reduce(
|
|
/**
|
|
* @param {string[]} acc
|
|
* @param {ParamInfo} cur
|
|
* @returns {string[]}
|
|
*/
|
|
(acc, cur) => {
|
|
if (Array.isArray(cur)) {
|
|
let nms;
|
|
if (Array.isArray(cur[1])) {
|
|
nms = cur[1];
|
|
} else {
|
|
if (cur[1].hasRestElement) {
|
|
hasRestElement = true;
|
|
}
|
|
|
|
if (cur[1].hasPropertyRest) {
|
|
hasPropertyRest = true;
|
|
}
|
|
|
|
nms = cur[1].names;
|
|
}
|
|
|
|
const flattened = flattenRoots(nms, root ? `${root}.${cur[0]}` : cur[0]);
|
|
if (flattened.hasRestElement) {
|
|
hasRestElement = true;
|
|
}
|
|
|
|
if (flattened.hasPropertyRest) {
|
|
hasPropertyRest = true;
|
|
}
|
|
|
|
const inner = /** @type {string[]} */ ([
|
|
root ? `${root}.${cur[0]}` : cur[0],
|
|
...flattened.names,
|
|
].filter(Boolean));
|
|
rests.push(false, ...flattened.rests);
|
|
|
|
return acc.concat(inner);
|
|
}
|
|
|
|
if (typeof cur === 'object') {
|
|
if ('isRestProperty' in cur && cur.isRestProperty) {
|
|
hasPropertyRest = true;
|
|
rests.push(true);
|
|
} else {
|
|
rests.push(false);
|
|
}
|
|
|
|
if ('restElement' in cur && cur.restElement) {
|
|
hasRestElement = true;
|
|
}
|
|
|
|
acc.push(root ? `${root}.${String(cur.name)}` : String(cur.name));
|
|
} else if (typeof cur !== 'undefined') {
|
|
rests.push(false);
|
|
acc.push(root ? `${root}.${cur}` : cur);
|
|
}
|
|
|
|
return acc;
|
|
}, [],
|
|
);
|
|
|
|
return {
|
|
hasPropertyRest,
|
|
hasRestElement,
|
|
names,
|
|
rests,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {import('@typescript-eslint/types').TSESTree.TSIndexSignature|
|
|
* import('@typescript-eslint/types').TSESTree.TSConstructSignatureDeclaration|
|
|
* import('@typescript-eslint/types').TSESTree.TSCallSignatureDeclaration|
|
|
* import('@typescript-eslint/types').TSESTree.TSPropertySignature} propSignature
|
|
* @returns {undefined|string|[string, string[]]}
|
|
*/
|
|
const getPropertiesFromPropertySignature = (propSignature) => {
|
|
if (
|
|
propSignature.type === 'TSIndexSignature' ||
|
|
propSignature.type === 'TSConstructSignatureDeclaration' ||
|
|
propSignature.type === 'TSCallSignatureDeclaration'
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
if (propSignature.typeAnnotation && propSignature.typeAnnotation.typeAnnotation.type === 'TSTypeLiteral') {
|
|
return [
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
propSignature.key
|
|
).name,
|
|
propSignature.typeAnnotation.typeAnnotation.members.map((member) => {
|
|
return /** @type {string} */ (
|
|
getPropertiesFromPropertySignature(
|
|
/** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */ (
|
|
member
|
|
),
|
|
)
|
|
);
|
|
}),
|
|
];
|
|
}
|
|
|
|
return /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
propSignature.key
|
|
).name;
|
|
};
|
|
|
|
/**
|
|
* @param {ESTreeOrTypeScriptNode|null} functionNode
|
|
* @param {boolean} [checkDefaultObjects]
|
|
* @throws {Error}
|
|
* @returns {ParamNameInfo[]}
|
|
*/
|
|
const getFunctionParameterNames = (
|
|
functionNode, checkDefaultObjects,
|
|
) => {
|
|
/* eslint-disable complexity -- Temporary */
|
|
/**
|
|
* @param {import('estree').Identifier|import('estree').AssignmentPattern|
|
|
* import('estree').ObjectPattern|import('estree').Property|
|
|
* import('estree').RestElement|import('estree').ArrayPattern|
|
|
* import('@typescript-eslint/types').TSESTree.TSParameterProperty|
|
|
* import('@typescript-eslint/types').TSESTree.Property|
|
|
* import('@typescript-eslint/types').TSESTree.RestElement|
|
|
* import('@typescript-eslint/types').TSESTree.Identifier|
|
|
* import('@typescript-eslint/types').TSESTree.ObjectPattern|
|
|
* import('@typescript-eslint/types').TSESTree.BindingName|
|
|
* import('@typescript-eslint/types').TSESTree.Parameter
|
|
* } param
|
|
* @param {boolean} [isProperty]
|
|
* @returns {ParamNameInfo|[string, ParamNameInfo[]]}
|
|
*/
|
|
const getParamName = (param, isProperty) => {
|
|
/* eslint-enable complexity -- Temporary */
|
|
const hasLeftTypeAnnotation = 'left' in param && 'typeAnnotation' in param.left;
|
|
|
|
if ('typeAnnotation' in param || hasLeftTypeAnnotation) {
|
|
const typeAnnotation = hasLeftTypeAnnotation ?
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
param.left
|
|
).typeAnnotation :
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier|import('@typescript-eslint/types').TSESTree.ObjectPattern} */
|
|
(param).typeAnnotation;
|
|
|
|
if (typeAnnotation?.typeAnnotation?.type === 'TSTypeLiteral') {
|
|
const propertyNames = typeAnnotation.typeAnnotation.members.map((member) => {
|
|
return getPropertiesFromPropertySignature(
|
|
/** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */
|
|
(member),
|
|
);
|
|
});
|
|
|
|
const flattened = {
|
|
...flattenRoots(propertyNames),
|
|
annotationParamName: 'name' in param ? param.name : undefined,
|
|
};
|
|
const hasLeftName = 'left' in param && 'name' in param.left;
|
|
|
|
if ('name' in param || hasLeftName) {
|
|
return [
|
|
hasLeftName ?
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
param.left
|
|
).name :
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
param
|
|
).name,
|
|
flattened,
|
|
];
|
|
}
|
|
|
|
return [
|
|
undefined, flattened,
|
|
];
|
|
}
|
|
}
|
|
|
|
if ('name' in param) {
|
|
return param.name;
|
|
}
|
|
|
|
if ('left' in param && 'name' in param.left) {
|
|
return param.left.name;
|
|
}
|
|
|
|
if (
|
|
param.type === 'ObjectPattern' ||
|
|
('left' in param &&
|
|
(
|
|
param
|
|
).left.type === 'ObjectPattern')
|
|
) {
|
|
const properties = /** @type {import('@typescript-eslint/types').TSESTree.ObjectPattern} */ (
|
|
param
|
|
).properties ||
|
|
/** @type {import('estree').ObjectPattern} */
|
|
(
|
|
/** @type {import('@typescript-eslint/types').TSESTree.AssignmentPattern} */ (
|
|
param
|
|
).left
|
|
)?.properties;
|
|
const roots = properties.map((prop) => {
|
|
return getParamName(prop, true);
|
|
});
|
|
|
|
return [
|
|
undefined, flattenRoots(roots),
|
|
];
|
|
}
|
|
|
|
if (param.type === 'Property') {
|
|
switch (param.value.type) {
|
|
case 'ArrayPattern': {
|
|
return [
|
|
/** @type {import('estree').Identifier} */
|
|
(param.key).name,
|
|
/** @type {import('estree').ArrayPattern} */ (
|
|
param.value
|
|
).elements.map((prop, idx) => {
|
|
return {
|
|
name: idx,
|
|
restElement: prop?.type === 'RestElement',
|
|
};
|
|
}),
|
|
];
|
|
}
|
|
|
|
case 'ObjectPattern': {
|
|
return [
|
|
/** @type {import('estree').Identifier} */ (param.key).name,
|
|
/** @type {import('estree').ObjectPattern} */ (
|
|
param.value
|
|
).properties.map((prop) => {
|
|
return /** @type {string|[string, string[]]} */ (getParamName(prop, isProperty));
|
|
}),
|
|
];
|
|
}
|
|
|
|
case 'AssignmentPattern': {
|
|
switch (param.value.left.type) {
|
|
case 'Identifier':
|
|
// Default parameter
|
|
if (checkDefaultObjects && param.value.right.type === 'ObjectExpression') {
|
|
return [
|
|
/** @type {import('estree').Identifier} */ (
|
|
param.key
|
|
).name,
|
|
/** @type {import('estree').AssignmentPattern} */ (
|
|
param.value
|
|
).right.properties.map((prop) => {
|
|
return /** @type {string} */ (getParamName(
|
|
/** @type {import('estree').Property} */
|
|
(prop),
|
|
isProperty,
|
|
));
|
|
}),
|
|
];
|
|
}
|
|
|
|
break;
|
|
case 'ObjectPattern':
|
|
return [
|
|
/** @type {import('estree').Identifier} */
|
|
(param.key).name,
|
|
/** @type {import('estree').ObjectPattern} */ (
|
|
param.value.left
|
|
).properties.map((prop) => {
|
|
return getParamName(prop, isProperty);
|
|
}),
|
|
];
|
|
case 'ArrayPattern':
|
|
return [
|
|
/** @type {import('estree').Identifier} */
|
|
(param.key).name,
|
|
/** @type {import('estree').ArrayPattern} */ (
|
|
param.value.left
|
|
).elements.map((prop, idx) => {
|
|
return {
|
|
name: idx,
|
|
restElement: prop?.type === 'RestElement',
|
|
};
|
|
}),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (param.key.type) {
|
|
case 'Identifier':
|
|
return param.key.name;
|
|
|
|
// The key of an object could also be a string or number
|
|
case 'Literal':
|
|
/* c8 ignore next 2 -- `raw` may not be present in all parsers */
|
|
return /** @type {string} */ (param.key.raw ||
|
|
param.key.value);
|
|
|
|
// case 'MemberExpression':
|
|
default:
|
|
// Todo: We should really create a structure (and a corresponding
|
|
// option analogous to `checkRestProperty`) which allows for
|
|
// (and optionally requires) dynamic properties to have a single
|
|
// line of documentation
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
if (
|
|
param.type === 'ArrayPattern' ||
|
|
/** @type {import('estree').AssignmentPattern} */ (
|
|
param
|
|
).left?.type === 'ArrayPattern'
|
|
) {
|
|
const elements = /** @type {import('estree').ArrayPattern} */ (
|
|
param
|
|
).elements || /** @type {import('estree').ArrayPattern} */ (
|
|
/** @type {import('estree').AssignmentPattern} */ (
|
|
param
|
|
).left
|
|
)?.elements;
|
|
const roots = elements.map((prop, idx) => {
|
|
return {
|
|
name: `"${idx}"`,
|
|
restElement: prop?.type === 'RestElement',
|
|
};
|
|
});
|
|
|
|
return [
|
|
undefined, flattenRoots(roots),
|
|
];
|
|
}
|
|
|
|
if ([
|
|
'RestElement', 'ExperimentalRestProperty',
|
|
].includes(param.type)) {
|
|
return {
|
|
isRestProperty: isProperty,
|
|
name: /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
/** @type {import('@typescript-eslint/types').TSESTree.RestElement} */ (
|
|
param
|
|
// @ts-expect-error Ok
|
|
).argument).name ?? param?.argument?.elements?.map(({name}) => {
|
|
return name;
|
|
}),
|
|
restElement: true,
|
|
};
|
|
}
|
|
|
|
if (param.type === 'TSParameterProperty') {
|
|
return getParamName(
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
/** @type {import('@typescript-eslint/types').TSESTree.TSParameterProperty} */ (
|
|
param
|
|
).parameter
|
|
),
|
|
true,
|
|
);
|
|
}
|
|
|
|
throw new Error(`Unsupported function signature format: \`${param.type}\`.`);
|
|
};
|
|
|
|
if (!functionNode) {
|
|
return [];
|
|
}
|
|
|
|
return (/** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
|
|
functionNode
|
|
).params || /** @type {import('@typescript-eslint/types').TSESTree.MethodDefinition} */ (
|
|
functionNode
|
|
).value?.params || []).map((param) => {
|
|
return getParamName(param);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {ESTreeOrTypeScriptNode} functionNode
|
|
* @returns {Integer}
|
|
*/
|
|
const hasParams = (functionNode) => {
|
|
// Should also check `functionNode.value.params` if supporting `MethodDefinition`
|
|
return /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
|
|
functionNode
|
|
).params.length;
|
|
};
|
|
|
|
/**
|
|
* Gets all names of the target type, including those that refer to a path, e.g.
|
|
* `foo` or `foo.bar`.
|
|
* @param {import('comment-parser').Block} jsdoc
|
|
* @param {string} targetTagName
|
|
* @returns {{
|
|
* idx: Integer,
|
|
* name: string,
|
|
* type: string
|
|
* }[]}
|
|
*/
|
|
const getJsdocTagsDeep = (jsdoc, targetTagName) => {
|
|
const ret = [];
|
|
for (const [
|
|
idx,
|
|
{
|
|
name,
|
|
tag,
|
|
type,
|
|
},
|
|
] of jsdoc.tags.entries()) {
|
|
if (tag !== targetTagName) {
|
|
continue;
|
|
}
|
|
|
|
ret.push({
|
|
idx,
|
|
name,
|
|
type,
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
const modeWarnSettings = WarnSettings();
|
|
|
|
/**
|
|
* @param {ParserMode|undefined} mode
|
|
* @param {Reporter} context
|
|
* @returns {import('./tagNames.js').AliasedTags}
|
|
*/
|
|
const getTagNamesForMode = (mode, context) => {
|
|
switch (mode) {
|
|
case 'jsdoc':
|
|
return jsdocTags;
|
|
case 'typescript':
|
|
return typeScriptTags;
|
|
case 'closure': case 'permissive':
|
|
return closureTags;
|
|
default:
|
|
if (!modeWarnSettings.hasBeenWarned(context, 'mode')) {
|
|
context.report({
|
|
loc: {
|
|
end: {
|
|
column: 1,
|
|
line: 1,
|
|
},
|
|
start: {
|
|
column: 1,
|
|
line: 1,
|
|
},
|
|
},
|
|
message: `Unrecognized value \`${mode}\` for \`settings.jsdoc.mode\`.`,
|
|
});
|
|
modeWarnSettings.markSettingAsWarned(context, 'mode');
|
|
}
|
|
|
|
// We'll avoid breaking too many other rules
|
|
return jsdocTags;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {import('comment-parser').Spec} tg
|
|
* @param {boolean} [returnArray]
|
|
* @returns {string[]|string}
|
|
*/
|
|
const getTagDescription = (tg, returnArray) => {
|
|
/**
|
|
* @type {string[]}
|
|
*/
|
|
const descriptions = [];
|
|
tg.source.some(({
|
|
tokens: {
|
|
end,
|
|
lineEnd,
|
|
postDelimiter,
|
|
tag,
|
|
postTag,
|
|
name,
|
|
type,
|
|
description,
|
|
},
|
|
}) => {
|
|
const desc = (
|
|
tag && postTag ||
|
|
!tag && !name && !type && postDelimiter || ''
|
|
|
|
// Remove space
|
|
).slice(1) +
|
|
(description || '') + (lineEnd || '');
|
|
|
|
if (end) {
|
|
if (desc) {
|
|
descriptions.push(desc);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
descriptions.push(desc);
|
|
|
|
return false;
|
|
});
|
|
|
|
return returnArray ? descriptions : descriptions.join('\n');
|
|
};
|
|
|
|
/**
|
|
* @typedef {{
|
|
* report: (descriptor: import('eslint').Rule.ReportDescriptor) => void
|
|
* }} Reporter
|
|
*/
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {ParserMode|undefined} mode
|
|
* @param {TagNamePreference} tagPreference
|
|
* @param {Reporter} context
|
|
* @returns {string|false|{
|
|
* message: string;
|
|
* replacement?: string|undefined;
|
|
* }}
|
|
*/
|
|
const getPreferredTagNameSimple = (
|
|
name,
|
|
mode,
|
|
tagPreference = {},
|
|
context = {
|
|
report () {
|
|
// No-op
|
|
}
|
|
},
|
|
) => {
|
|
const prefValues = Object.values(tagPreference);
|
|
if (prefValues.includes(name) || prefValues.some((prefVal) => {
|
|
return prefVal && typeof prefVal === 'object' && prefVal.replacement === name;
|
|
})) {
|
|
return name;
|
|
}
|
|
|
|
// Allow keys to have a 'tag ' prefix to avoid upstream bug in ESLint
|
|
// that disallows keys that conflict with Object.prototype,
|
|
// e.g. 'tag constructor' for 'constructor':
|
|
// https://github.com/eslint/eslint/issues/13289
|
|
// https://github.com/gajus/eslint-plugin-jsdoc/issues/537
|
|
const tagPreferenceFixed = Object.fromEntries(
|
|
Object
|
|
.entries(tagPreference)
|
|
.map(([
|
|
key,
|
|
value,
|
|
]) => {
|
|
return [
|
|
key.replace(/^tag /u, ''), value,
|
|
];
|
|
}),
|
|
);
|
|
|
|
if (Object.prototype.hasOwnProperty.call(tagPreferenceFixed, name)) {
|
|
return tagPreferenceFixed[name];
|
|
}
|
|
|
|
const tagNames = getTagNamesForMode(mode, context);
|
|
|
|
const preferredTagName = Object.entries(tagNames).find(([
|
|
, aliases,
|
|
]) => {
|
|
return aliases.includes(name);
|
|
})?.[0];
|
|
if (preferredTagName) {
|
|
return preferredTagName;
|
|
}
|
|
|
|
return name;
|
|
};
|
|
|
|
/**
|
|
* @param {import('eslint').Rule.RuleContext} context
|
|
* @param {ParserMode|undefined} mode
|
|
* @param {string} name
|
|
* @param {string[]} definedTags
|
|
* @returns {boolean}
|
|
*/
|
|
const isValidTag = (
|
|
context,
|
|
mode,
|
|
name,
|
|
definedTags,
|
|
) => {
|
|
const tagNames = getTagNamesForMode(mode, context);
|
|
|
|
const validTagNames = Object.keys(tagNames).concat(Object.values(tagNames).flat());
|
|
const additionalTags = definedTags;
|
|
const allTags = validTagNames.concat(additionalTags);
|
|
|
|
return allTags.includes(name);
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @param {string} targetTagName
|
|
* @returns {boolean}
|
|
*/
|
|
const hasTag = (jsdoc, targetTagName) => {
|
|
const targetTagLower = targetTagName.toLowerCase();
|
|
|
|
return jsdoc.tags.some((doc) => {
|
|
return doc.tag.toLowerCase() === targetTagLower;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @param {(tag: import('@es-joy/jsdoccomment').JsdocTagWithInline) => boolean} filter
|
|
* @returns {import('@es-joy/jsdoccomment').JsdocTagWithInline[]}
|
|
*/
|
|
const filterTags = (jsdoc, filter) => {
|
|
return jsdoc.tags.filter((tag) => {
|
|
return filter(tag);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @param {string} tagName
|
|
* @returns {import('comment-parser').Spec[]}
|
|
*/
|
|
const getTags = (jsdoc, tagName) => {
|
|
return filterTags(jsdoc, (item) => {
|
|
return item.tag === tagName;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @param {{
|
|
* tagName: string,
|
|
* context?: import('eslint').Rule.RuleContext,
|
|
* mode?: ParserMode,
|
|
* report?: import('./iterateJsdoc.js').Report
|
|
* tagNamePreference?: TagNamePreference
|
|
* skipReportingBlockedTag?: boolean,
|
|
* allowObjectReturn?: boolean,
|
|
* defaultMessage?: string,
|
|
* }} cfg
|
|
* @returns {string|undefined|false|{
|
|
* message: string;
|
|
* replacement?: string|undefined;
|
|
* }|{
|
|
* blocked: true,
|
|
* tagName: string
|
|
* }}
|
|
*/
|
|
const getPreferredTagName = (jsdoc, {
|
|
tagName,
|
|
context, mode,
|
|
tagNamePreference,
|
|
report = () => {},
|
|
skipReportingBlockedTag = false,
|
|
allowObjectReturn = false,
|
|
defaultMessage = `Unexpected tag \`@${tagName}\``,
|
|
}) => {
|
|
const ret = getPreferredTagNameSimple(tagName, mode, tagNamePreference, context);
|
|
const isObject = ret && typeof ret === 'object';
|
|
if (hasTag(jsdoc, tagName) && (ret === false || isObject && !ret.replacement)) {
|
|
if (skipReportingBlockedTag) {
|
|
return {
|
|
blocked: true,
|
|
tagName,
|
|
};
|
|
}
|
|
|
|
const message = isObject && ret.message || defaultMessage;
|
|
report(message, null, getTags(jsdoc, tagName)[0]);
|
|
|
|
return false;
|
|
}
|
|
|
|
return isObject && !allowObjectReturn ? ret.replacement : ret;
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @param {string} tagName
|
|
* @param {(
|
|
* matchingJsdocTag: import('@es-joy/jsdoccomment').JsdocTagWithInline,
|
|
* targetTagName: string
|
|
* ) => void} arrayHandler
|
|
* @param {object} cfg
|
|
* @param {import('eslint').Rule.RuleContext} [cfg.context]
|
|
* @param {ParserMode} [cfg.mode]
|
|
* @param {import('./iterateJsdoc.js').Report} [cfg.report]
|
|
* @param {TagNamePreference} [cfg.tagNamePreference]
|
|
* @param {boolean} [cfg.skipReportingBlockedTag]
|
|
* @returns {void}
|
|
*/
|
|
const forEachPreferredTag = (
|
|
jsdoc, tagName, arrayHandler,
|
|
{
|
|
context, mode, report,
|
|
tagNamePreference,
|
|
skipReportingBlockedTag = false,
|
|
} = {}
|
|
) => {
|
|
const targetTagName = /** @type {string|false} */ (
|
|
getPreferredTagName(jsdoc, {
|
|
skipReportingBlockedTag,
|
|
tagName,
|
|
context, mode, report, tagNamePreference
|
|
})
|
|
);
|
|
if (!targetTagName ||
|
|
skipReportingBlockedTag && targetTagName && typeof targetTagName === 'object'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const matchingJsdocTags = jsdoc.tags.filter(({
|
|
tag,
|
|
}) => {
|
|
return tag === targetTagName;
|
|
});
|
|
|
|
for (const matchingJsdocTag of matchingJsdocTags) {
|
|
arrayHandler(
|
|
/**
|
|
* @type {import('@es-joy/jsdoccomment').JsdocTagWithInline}
|
|
*/ (
|
|
matchingJsdocTag
|
|
), targetTagName,
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get all tags, inline tags and inline tags in tags
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @returns {(import('comment-parser').Spec|
|
|
* import('@es-joy/jsdoccomment').JsdocInlineTagNoType)[]}
|
|
*/
|
|
const getAllTags = (jsdoc) => {
|
|
return [
|
|
...jsdoc.tags,
|
|
...jsdoc.inlineTags.map((inlineTag) => {
|
|
// Tags don't have source or line numbers, so add before returning
|
|
let line = -1;
|
|
for (const {
|
|
tokens: {
|
|
description,
|
|
},
|
|
} of jsdoc.source) {
|
|
line++;
|
|
if (description && description.includes(`{@${inlineTag.tag}`)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
inlineTag.line = line;
|
|
|
|
return inlineTag;
|
|
}),
|
|
...jsdoc.tags.flatMap((tag) => {
|
|
let tagBegins = -1;
|
|
for (const {
|
|
tokens: {
|
|
tag: tg,
|
|
},
|
|
} of jsdoc.source) {
|
|
tagBegins++;
|
|
if (tg) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (const inlineTag of tag.inlineTags) {
|
|
/** @type {import('./iterateJsdoc.js').Integer} */
|
|
let line = 0;
|
|
for (const {
|
|
number,
|
|
tokens: {
|
|
description,
|
|
},
|
|
} of tag.source) {
|
|
if (description && description.includes(`{@${inlineTag.tag}`)) {
|
|
line = number;
|
|
break;
|
|
}
|
|
}
|
|
|
|
inlineTag.line = tagBegins + line - 1;
|
|
}
|
|
|
|
return (
|
|
/**
|
|
* @type {import('comment-parser').Spec & {
|
|
* inlineTags: import('@es-joy/jsdoccomment').JsdocInlineTagNoType[]
|
|
* }}
|
|
*/ (
|
|
tag
|
|
).inlineTags
|
|
);
|
|
}),
|
|
];
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @param {string[]} targetTagNames
|
|
* @returns {boolean}
|
|
*/
|
|
const hasATag = (jsdoc, targetTagNames) => {
|
|
return targetTagNames.some((targetTagName) => {
|
|
return hasTag(jsdoc, targetTagName);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Checks if the JSDoc comment has an undefined type.
|
|
* @param {import('comment-parser').Spec|null|undefined} tag
|
|
* the tag which should be checked.
|
|
* @param {ParserMode} mode
|
|
* @returns {boolean}
|
|
* true in case a defined type is undeclared; otherwise false.
|
|
*/
|
|
const mayBeUndefinedTypeTag = (tag, mode) => {
|
|
// The function should not continue in the event the type is not defined...
|
|
if (typeof tag === 'undefined' || tag === null) {
|
|
return true;
|
|
}
|
|
|
|
// .. same applies if it declares an `{undefined}` or `{void}` type
|
|
const tagType = tag.type.trim();
|
|
|
|
// Exit early if matching
|
|
if (
|
|
tagType === 'undefined' || tagType === 'void' ||
|
|
tagType === '*' || tagType === 'any'
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
let parsedTypes;
|
|
try {
|
|
parsedTypes = tryParse(
|
|
tagType,
|
|
mode === 'permissive' ? undefined : [
|
|
mode,
|
|
],
|
|
);
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
|
|
if (
|
|
// We do not traverse deeply as it could be, e.g., `Promise<void>`
|
|
parsedTypes &&
|
|
parsedTypes.type === 'JsdocTypeUnion' &&
|
|
parsedTypes.elements.find((elem) => {
|
|
return elem.type === 'JsdocTypeUndefined' ||
|
|
elem.type === 'JsdocTypeName' && elem.value === 'void';
|
|
})) {
|
|
return true;
|
|
}
|
|
|
|
// In any other case, a type is present
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} map
|
|
* @param {string} tag
|
|
* @returns {Map<string, string|string[]|boolean|undefined>}
|
|
*/
|
|
const ensureMap = (map, tag) => {
|
|
if (!map.has(tag)) {
|
|
map.set(tag, new Map());
|
|
}
|
|
|
|
return /** @type {Map<string, string | boolean>} */ (map.get(tag));
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').StructuredTags} structuredTags
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {void}
|
|
*/
|
|
const overrideTagStructure = (structuredTags, tagMap = tagStructure) => {
|
|
for (const [
|
|
tag,
|
|
{
|
|
name,
|
|
type,
|
|
required = [],
|
|
},
|
|
] of Object.entries(structuredTags)) {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
tagStruct.set('namepathRole', name);
|
|
tagStruct.set('typeAllowed', type);
|
|
|
|
const requiredName = required.includes('name');
|
|
if (requiredName && name === false) {
|
|
throw new Error('Cannot add "name" to `require` with the tag\'s `name` set to `false`');
|
|
}
|
|
|
|
tagStruct.set('nameRequired', requiredName);
|
|
|
|
const requiredType = required.includes('type');
|
|
if (requiredType && type === false) {
|
|
throw new Error('Cannot add "type" to `require` with the tag\'s `type` set to `false`');
|
|
}
|
|
|
|
tagStruct.set('typeRequired', requiredType);
|
|
|
|
const typeOrNameRequired = required.includes('typeOrNameRequired');
|
|
if (typeOrNameRequired && name === false) {
|
|
throw new Error('Cannot add "typeOrNameRequired" to `require` with the tag\'s `name` set to `false`');
|
|
}
|
|
|
|
if (typeOrNameRequired && type === false) {
|
|
throw new Error('Cannot add "typeOrNameRequired" to `require` with the tag\'s `type` set to `false`');
|
|
}
|
|
|
|
tagStruct.set('typeOrNameRequired', typeOrNameRequired);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {ParserMode} mode
|
|
* @param {import('./iterateJsdoc.js').StructuredTags} structuredTags
|
|
* @returns {import('./getDefaultTagStructureForMode.js').TagStructure}
|
|
*/
|
|
const getTagStructureForMode = (mode, structuredTags) => {
|
|
const tagStruct = getDefaultTagStructureForMode(mode);
|
|
|
|
try {
|
|
overrideTagStructure(structuredTags, tagStruct);
|
|
/* c8 ignore next 3 */
|
|
} catch {
|
|
//
|
|
}
|
|
|
|
return tagStruct;
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean}
|
|
*/
|
|
const isNamepathDefiningTag = (tag, tagMap = tagStructure) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
return tagStruct.get('namepathRole') === 'namepath-defining';
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean}
|
|
*/
|
|
const isNamepathReferencingTag = (tag, tagMap = tagStructure) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
return tagStruct.get('namepathRole') === 'namepath-referencing';
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean}
|
|
*/
|
|
const isNamepathOrUrlReferencingTag = (tag, tagMap = tagStructure) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
return tagStruct.get('namepathRole') === 'namepath-or-url-referencing';
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean|undefined}
|
|
*/
|
|
const tagMustHaveTypePosition = (tag, tagMap = tagStructure) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
return /** @type {boolean|undefined} */ (tagStruct.get('typeRequired'));
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean|string}
|
|
*/
|
|
const tagMightHaveTypePosition = (tag, tagMap = tagStructure) => {
|
|
if (tagMustHaveTypePosition(tag, tagMap)) {
|
|
return true;
|
|
}
|
|
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
const ret = /** @type {boolean|undefined} */ (tagStruct.get('typeAllowed'));
|
|
|
|
return ret === undefined ? true : ret;
|
|
};
|
|
|
|
const namepathTypes = new Set([
|
|
'namepath-defining', 'namepath-referencing',
|
|
]);
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean}
|
|
*/
|
|
const tagMightHaveNamePosition = (tag, tagMap = tagStructure) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
const ret = tagStruct.get('namepathRole');
|
|
|
|
return ret === undefined ? true : Boolean(ret);
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean}
|
|
*/
|
|
const tagMightHaveNamepath = (tag, tagMap = tagStructure) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
const nampathRole = tagStruct.get('namepathRole');
|
|
|
|
return nampathRole !== false &&
|
|
namepathTypes.has(/** @type {string} */ (nampathRole));
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean|undefined}
|
|
*/
|
|
const tagMustHaveNamePosition = (tag, tagMap = tagStructure) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
return /** @type {boolean|undefined} */ (tagStruct.get('nameRequired'));
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean}
|
|
*/
|
|
const tagMightHaveEitherTypeOrNamePosition = (tag, tagMap) => {
|
|
return Boolean(tagMightHaveTypePosition(tag, tagMap)) || tagMightHaveNamepath(tag, tagMap);
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean|undefined}
|
|
*/
|
|
const tagMustHaveEitherTypeOrNamePosition = (tag, tagMap) => {
|
|
const tagStruct = ensureMap(tagMap, tag);
|
|
|
|
return /** @type {boolean} */ (tagStruct.get('typeOrNameRequired'));
|
|
};
|
|
|
|
/**
|
|
* @param {import('comment-parser').Spec} tag
|
|
* @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
|
|
* @returns {boolean|undefined}
|
|
*/
|
|
const tagMissingRequiredTypeOrNamepath = (tag, tagMap = tagStructure) => {
|
|
const mustHaveTypePosition = tagMustHaveTypePosition(tag.tag, tagMap);
|
|
const mightHaveTypePosition = tagMightHaveTypePosition(tag.tag, tagMap);
|
|
const hasTypePosition = mightHaveTypePosition && Boolean(tag.type);
|
|
const hasNameOrNamepathPosition = (
|
|
tagMustHaveNamePosition(tag.tag, tagMap) ||
|
|
tagMightHaveNamepath(tag.tag, tagMap)
|
|
) && Boolean(tag.name);
|
|
const mustHaveEither = tagMustHaveEitherTypeOrNamePosition(tag.tag, tagMap);
|
|
const hasEither = tagMightHaveEitherTypeOrNamePosition(tag.tag, tagMap) &&
|
|
(hasTypePosition || hasNameOrNamepathPosition);
|
|
|
|
return mustHaveEither && !hasEither && !mustHaveTypePosition;
|
|
};
|
|
|
|
/* eslint-disable complexity -- Temporary */
|
|
/**
|
|
* @param {ESTreeOrTypeScriptNode|null|undefined} node
|
|
* @param {boolean} [checkYieldReturnValue]
|
|
* @returns {boolean}
|
|
*/
|
|
const hasNonFunctionYield = (node, checkYieldReturnValue) => {
|
|
/* eslint-enable complexity -- Temporary */
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
|
|
switch (node.type) {
|
|
case 'BlockStatement': {
|
|
return node.body.some((bodyNode) => {
|
|
return ![
|
|
'ArrowFunctionExpression',
|
|
'FunctionDeclaration',
|
|
'FunctionExpression',
|
|
].includes(bodyNode.type) && hasNonFunctionYield(
|
|
bodyNode, checkYieldReturnValue,
|
|
);
|
|
});
|
|
}
|
|
|
|
/* c8 ignore next 2 -- In Babel? */
|
|
// @ts-expect-error In Babel?
|
|
case 'OptionalCallExpression':
|
|
case 'CallExpression':
|
|
return node.arguments.some((element) => {
|
|
return hasNonFunctionYield(element, checkYieldReturnValue);
|
|
});
|
|
case 'ChainExpression':
|
|
case 'ExpressionStatement': {
|
|
return hasNonFunctionYield(node.expression, checkYieldReturnValue);
|
|
}
|
|
|
|
case 'LabeledStatement':
|
|
case 'WhileStatement':
|
|
case 'DoWhileStatement':
|
|
case 'ForStatement':
|
|
case 'ForInStatement':
|
|
case 'ForOfStatement':
|
|
case 'WithStatement': {
|
|
return hasNonFunctionYield(node.body, checkYieldReturnValue);
|
|
}
|
|
|
|
case 'ConditionalExpression':
|
|
case 'IfStatement': {
|
|
return hasNonFunctionYield(node.test, checkYieldReturnValue) ||
|
|
hasNonFunctionYield(node.consequent, checkYieldReturnValue) ||
|
|
hasNonFunctionYield(node.alternate, checkYieldReturnValue);
|
|
}
|
|
|
|
case 'TryStatement': {
|
|
return hasNonFunctionYield(node.block, checkYieldReturnValue) ||
|
|
hasNonFunctionYield(
|
|
node.handler && node.handler.body, checkYieldReturnValue,
|
|
) ||
|
|
hasNonFunctionYield(
|
|
/** @type {import('@typescript-eslint/types').TSESTree.BlockStatement} */
|
|
(node.finalizer),
|
|
checkYieldReturnValue,
|
|
);
|
|
}
|
|
|
|
case 'SwitchStatement': {
|
|
return node.cases.some(
|
|
(someCase) => {
|
|
return someCase.consequent.some((nde) => {
|
|
return hasNonFunctionYield(nde, checkYieldReturnValue);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
case 'ArrayPattern':
|
|
case 'ArrayExpression':
|
|
return node.elements.some((element) => {
|
|
return hasNonFunctionYield(element, checkYieldReturnValue);
|
|
});
|
|
case 'AssignmentPattern':
|
|
return hasNonFunctionYield(node.right, checkYieldReturnValue);
|
|
|
|
case 'VariableDeclaration': {
|
|
return node.declarations.some((nde) => {
|
|
return hasNonFunctionYield(nde, checkYieldReturnValue);
|
|
});
|
|
}
|
|
|
|
case 'VariableDeclarator': {
|
|
return hasNonFunctionYield(node.id, checkYieldReturnValue) ||
|
|
hasNonFunctionYield(node.init, checkYieldReturnValue);
|
|
}
|
|
|
|
case 'AssignmentExpression':
|
|
case 'BinaryExpression':
|
|
case 'LogicalExpression': {
|
|
return hasNonFunctionYield(node.left, checkYieldReturnValue) ||
|
|
hasNonFunctionYield(node.right, checkYieldReturnValue);
|
|
}
|
|
|
|
// Comma
|
|
case 'SequenceExpression':
|
|
case 'TemplateLiteral':
|
|
return node.expressions.some((subExpression) => {
|
|
return hasNonFunctionYield(subExpression, checkYieldReturnValue);
|
|
});
|
|
|
|
case 'ObjectPattern':
|
|
case 'ObjectExpression':
|
|
return node.properties.some((property) => {
|
|
return hasNonFunctionYield(property, checkYieldReturnValue);
|
|
});
|
|
|
|
/* c8 ignore next -- In Babel? */
|
|
case 'PropertyDefinition':
|
|
/* eslint-disable no-fallthrough */
|
|
/* c8 ignore next 2 -- In Babel? */
|
|
// @ts-expect-error In Babel?
|
|
case 'ObjectProperty':
|
|
/* c8 ignore next 2 -- In Babel? */
|
|
// @ts-expect-error In Babel?
|
|
case 'ClassProperty':
|
|
case 'Property':
|
|
/* eslint-enable no-fallthrough */
|
|
return node.computed && hasNonFunctionYield(node.key, checkYieldReturnValue) ||
|
|
hasNonFunctionYield(node.value, checkYieldReturnValue);
|
|
/* c8 ignore next 2 -- In Babel? */
|
|
// @ts-expect-error In Babel?
|
|
case 'ObjectMethod':
|
|
/* c8 ignore next 6 -- In Babel? */
|
|
// @ts-expect-error In Babel?
|
|
return node.computed && hasNonFunctionYield(node.key, checkYieldReturnValue) ||
|
|
// @ts-expect-error In Babel?
|
|
node.arguments.some((nde) => {
|
|
return hasNonFunctionYield(nde, checkYieldReturnValue);
|
|
});
|
|
|
|
case 'SpreadElement':
|
|
case 'UnaryExpression':
|
|
return hasNonFunctionYield(node.argument, checkYieldReturnValue);
|
|
|
|
case 'TaggedTemplateExpression':
|
|
return hasNonFunctionYield(node.quasi, checkYieldReturnValue);
|
|
|
|
// ?.
|
|
/* c8 ignore next 2 -- In Babel? */
|
|
// @ts-expect-error In Babel?
|
|
case 'OptionalMemberExpression':
|
|
case 'MemberExpression':
|
|
return hasNonFunctionYield(node.object, checkYieldReturnValue) ||
|
|
hasNonFunctionYield(node.property, checkYieldReturnValue);
|
|
|
|
/* c8 ignore next 2 -- In Babel? */
|
|
// @ts-expect-error In Babel?
|
|
case 'Import':
|
|
case 'ImportExpression':
|
|
return hasNonFunctionYield(node.source, checkYieldReturnValue);
|
|
|
|
case 'ReturnStatement': {
|
|
if (node.argument === null) {
|
|
return false;
|
|
}
|
|
|
|
return hasNonFunctionYield(node.argument, checkYieldReturnValue);
|
|
}
|
|
|
|
case 'YieldExpression': {
|
|
if (checkYieldReturnValue) {
|
|
if (
|
|
/** @type {import('eslint').Rule.Node} */ (
|
|
node
|
|
).parent.type === 'VariableDeclarator'
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// void return does not count.
|
|
if (node.argument === null) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks if a node has a return statement. Void return does not count.
|
|
* @param {ESTreeOrTypeScriptNode} node
|
|
* @param {boolean} [checkYieldReturnValue]
|
|
* @returns {boolean}
|
|
*/
|
|
const hasYieldValue = (node, checkYieldReturnValue) => {
|
|
return /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
|
|
node
|
|
).generator && (
|
|
/** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
|
|
node
|
|
).expression || hasNonFunctionYield(
|
|
/** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */
|
|
(node).body,
|
|
checkYieldReturnValue,
|
|
)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Checks if a node has a throws statement.
|
|
* @param {ESTreeOrTypeScriptNode|null|undefined} node
|
|
* @param {boolean} [innerFunction]
|
|
* @returns {boolean}
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
const hasThrowValue = (node, innerFunction) => {
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
|
|
// There are cases where a function may execute its inner function which
|
|
// throws, but we're treating functions atomically rather than trying to
|
|
// follow them
|
|
switch (node.type) {
|
|
case 'FunctionExpression':
|
|
case 'FunctionDeclaration':
|
|
case 'ArrowFunctionExpression': {
|
|
return !innerFunction && !node.async && hasThrowValue(node.body, true);
|
|
}
|
|
|
|
case 'BlockStatement': {
|
|
return node.body.some((bodyNode) => {
|
|
return bodyNode.type !== 'FunctionDeclaration' && hasThrowValue(bodyNode);
|
|
});
|
|
}
|
|
|
|
case 'LabeledStatement':
|
|
case 'WhileStatement':
|
|
case 'DoWhileStatement':
|
|
case 'ForStatement':
|
|
case 'ForInStatement':
|
|
case 'ForOfStatement':
|
|
case 'WithStatement': {
|
|
return hasThrowValue(node.body);
|
|
}
|
|
|
|
case 'IfStatement': {
|
|
return hasThrowValue(node.consequent) || hasThrowValue(node.alternate);
|
|
}
|
|
|
|
// We only consider it to throw an error if the catch or finally blocks throw an error.
|
|
case 'TryStatement': {
|
|
return hasThrowValue(node.handler && node.handler.body) ||
|
|
hasThrowValue(node.finalizer);
|
|
}
|
|
|
|
case 'SwitchStatement': {
|
|
return node.cases.some(
|
|
(someCase) => {
|
|
return someCase.consequent.some((nde) => {
|
|
return hasThrowValue(nde);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
case 'ThrowStatement': {
|
|
return true;
|
|
}
|
|
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} tag
|
|
*/
|
|
/*
|
|
const isInlineTag = (tag) => {
|
|
return /^(@link|@linkcode|@linkplain|@tutorial) /u.test(tag);
|
|
};
|
|
*/
|
|
|
|
/**
|
|
* Parses GCC Generic/Template types
|
|
* @see {@link https://github.com/google/closure-compiler/wiki/Generic-Types}
|
|
* @see {@link https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#template}
|
|
* @param {import('comment-parser').Spec} tag
|
|
* @returns {string[]}
|
|
*/
|
|
const parseClosureTemplateTag = (tag) => {
|
|
return tag.name
|
|
.split(',')
|
|
.map((type) => {
|
|
return type.trim().replace(/^\[(?<name>.*?)=.*\]$/u, '$<name>');
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @typedef {true|string[]} DefaultContexts
|
|
*/
|
|
|
|
/**
|
|
* Checks user option for `contexts` array, defaulting to
|
|
* contexts designated by the rule. Returns an array of
|
|
* ESTree AST types, indicating allowable contexts.
|
|
* @param {import('eslint').Rule.RuleContext} context
|
|
* @param {DefaultContexts|undefined} defaultContexts
|
|
* @param {{
|
|
* contexts?: import('./iterateJsdoc.js').Context[]
|
|
* }} settings
|
|
* @returns {(string|import('./iterateJsdoc.js').ContextObject)[]}
|
|
*/
|
|
const enforcedContexts = (context, defaultContexts, settings) => {
|
|
const contexts = context.options[0]?.contexts || settings.contexts || (defaultContexts === true ? [
|
|
'ArrowFunctionExpression',
|
|
'FunctionDeclaration',
|
|
'FunctionExpression',
|
|
'TSDeclareFunction',
|
|
] : defaultContexts);
|
|
|
|
return contexts;
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').Context[]} contexts
|
|
* @param {import('./iterateJsdoc.js').CheckJsdoc} checkJsdoc
|
|
* @param {import('@es-joy/jsdoccomment').CommentHandler} [handler]
|
|
* @returns {import('eslint').Rule.RuleListener}
|
|
*/
|
|
const getContextObject = (contexts, checkJsdoc, handler) => {
|
|
/** @type {import('eslint').Rule.RuleListener} */
|
|
const properties = {};
|
|
|
|
for (const [
|
|
idx,
|
|
prop,
|
|
] of contexts.entries()) {
|
|
/** @type {string} */
|
|
let property;
|
|
|
|
/** @type {(node: import('eslint').Rule.Node) => void} */
|
|
let value;
|
|
|
|
if (typeof prop === 'object') {
|
|
const selInfo = {
|
|
lastIndex: idx,
|
|
selector: prop.context,
|
|
};
|
|
if (prop.comment) {
|
|
property = /** @type {string} */ (prop.context);
|
|
value = checkJsdoc.bind(
|
|
null,
|
|
{
|
|
...selInfo,
|
|
comment: prop.comment,
|
|
},
|
|
/**
|
|
* @type {(jsdoc: import('@es-joy/jsdoccomment').JsdocBlockWithInline) => boolean}
|
|
*/
|
|
(/** @type {import('@es-joy/jsdoccomment').CommentHandler} */ (
|
|
handler
|
|
).bind(null, prop.comment)),
|
|
);
|
|
} else {
|
|
property = /** @type {string} */ (prop.context);
|
|
value = checkJsdoc.bind(null, selInfo, null);
|
|
}
|
|
} else {
|
|
const selInfo = {
|
|
lastIndex: idx,
|
|
selector: prop,
|
|
};
|
|
property = prop;
|
|
value = checkJsdoc.bind(null, selInfo, null);
|
|
}
|
|
|
|
const old = /**
|
|
* @type {((node: import('eslint').Rule.Node) => void)}
|
|
*/ (properties[property]);
|
|
properties[property] = old ?
|
|
/**
|
|
* @type {((node: import('eslint').Rule.Node) => void)}
|
|
*/
|
|
function (node) {
|
|
old(node);
|
|
value(node);
|
|
} :
|
|
value;
|
|
}
|
|
|
|
return properties;
|
|
};
|
|
|
|
const tagsWithNamesAndDescriptions = new Set([
|
|
'param', 'arg', 'argument', 'property', 'prop',
|
|
'template',
|
|
|
|
// These two are parsed by our custom parser as though having a `name`
|
|
'returns', 'return',
|
|
]);
|
|
|
|
/**
|
|
* @typedef {{
|
|
* [key: string]: false|string|
|
|
* {message: string, replacement?: string}
|
|
* }} TagNamePreference
|
|
*/
|
|
|
|
/**
|
|
* @param {import('eslint').Rule.RuleContext} context
|
|
* @param {ParserMode|undefined} mode
|
|
* @param {import('comment-parser').Spec[]} tags
|
|
* @returns {{
|
|
* tagsWithNames: import('comment-parser').Spec[],
|
|
* tagsWithoutNames: import('comment-parser').Spec[]
|
|
* }}
|
|
*/
|
|
const getTagsByType = (context, mode, tags) => {
|
|
/**
|
|
* @type {import('comment-parser').Spec[]}
|
|
*/
|
|
const tagsWithoutNames = [];
|
|
const tagsWithNames = tags.filter((tag) => {
|
|
const {
|
|
tag: tagName,
|
|
} = tag;
|
|
const tagWithName = tagsWithNamesAndDescriptions.has(tagName);
|
|
if (!tagWithName) {
|
|
tagsWithoutNames.push(tag);
|
|
}
|
|
|
|
return tagWithName;
|
|
});
|
|
|
|
return {
|
|
tagsWithNames,
|
|
tagsWithoutNames,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {import('eslint').SourceCode|{
|
|
* text: string
|
|
* }} sourceCode
|
|
* @returns {string}
|
|
*/
|
|
const getIndent = (sourceCode) => {
|
|
return (sourceCode.text.match(/^\n*([ \t]+)/u)?.[1] ?? '') + ' ';
|
|
};
|
|
|
|
/**
|
|
* @param {import('eslint').Rule.Node|null} node
|
|
* @returns {boolean}
|
|
*/
|
|
const isConstructor = (node) => {
|
|
return node?.type === 'MethodDefinition' && node.kind === 'constructor' ||
|
|
/** @type {import('@typescript-eslint/types').TSESTree.MethodDefinition} */ (
|
|
node?.parent
|
|
)?.kind === 'constructor';
|
|
};
|
|
|
|
/**
|
|
* @param {import('eslint').Rule.Node|null} node
|
|
* @returns {boolean}
|
|
*/
|
|
const isGetter = (node) => {
|
|
return node !== null &&
|
|
/**
|
|
* @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
|
|
* import('@typescript-eslint/types').TSESTree.Property}
|
|
*/ (
|
|
node.parent
|
|
)?.kind === 'get';
|
|
};
|
|
|
|
/**
|
|
* @param {import('eslint').Rule.Node|null} node
|
|
* @returns {boolean}
|
|
*/
|
|
const isSetter = (node) => {
|
|
return node !== null &&
|
|
/**
|
|
* @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
|
|
* import('@typescript-eslint/types').TSESTree.Property}
|
|
*/(
|
|
node.parent
|
|
)?.kind === 'set';
|
|
};
|
|
|
|
/**
|
|
* @param {import('eslint').Rule.Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
const hasAccessorPair = (node) => {
|
|
const {
|
|
type,
|
|
kind: sourceKind,
|
|
key,
|
|
} =
|
|
/**
|
|
* @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
|
|
* import('@typescript-eslint/types').TSESTree.Property}
|
|
*/ (node);
|
|
|
|
const sourceName =
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
key
|
|
).name;
|
|
|
|
const oppositeKind = sourceKind === 'get' ? 'set' : 'get';
|
|
|
|
const sibling = type === 'MethodDefinition' ?
|
|
/** @type {import('@typescript-eslint/types').TSESTree.ClassBody} */ (
|
|
node.parent
|
|
).body :
|
|
/** @type {import('@typescript-eslint/types').TSESTree.ObjectExpression} */ (
|
|
node.parent
|
|
).properties;
|
|
|
|
return (
|
|
sibling.some((child) => {
|
|
const {
|
|
kind,
|
|
key: ky,
|
|
} = /**
|
|
* @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
|
|
* import('@typescript-eslint/types').TSESTree.Property}
|
|
*/ (child);
|
|
|
|
const name =
|
|
/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
|
|
ky
|
|
).name;
|
|
|
|
return kind === oppositeKind && name === sourceName;
|
|
})
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
* @param {import('eslint').Rule.Node|null} node
|
|
* @param {import('eslint').Rule.RuleContext} context
|
|
* @param {import('json-schema').JSONSchema4} schema
|
|
* @returns {boolean}
|
|
*/
|
|
const exemptSpeciaMethods = (jsdoc, node, context, schema) => {
|
|
/**
|
|
* @param {"checkGetters"|"checkSetters"|"checkConstructors"} prop
|
|
* @returns {boolean|"no-setter"|"no-getter"}
|
|
*/
|
|
const hasSchemaOption = (prop) => {
|
|
const schemaProperties = schema[0].properties;
|
|
|
|
return context.options[0]?.[prop] ??
|
|
(schemaProperties[prop] && schemaProperties[prop].default);
|
|
};
|
|
|
|
const checkGetters = hasSchemaOption('checkGetters');
|
|
const checkSetters = hasSchemaOption('checkSetters');
|
|
|
|
return !hasSchemaOption('checkConstructors') &&
|
|
(
|
|
isConstructor(node) ||
|
|
hasATag(jsdoc, [
|
|
'class',
|
|
'constructor',
|
|
])) ||
|
|
isGetter(node) && (
|
|
!checkGetters ||
|
|
checkGetters === 'no-setter' && hasAccessorPair(/** @type {import('./iterateJsdoc.js').Node} */ (node).parent)
|
|
) ||
|
|
isSetter(node) && (
|
|
!checkSetters ||
|
|
checkSetters === 'no-getter' && hasAccessorPair(/** @type {import('./iterateJsdoc.js').Node} */ (node).parent)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Since path segments may be unquoted (if matching a reserved word,
|
|
* identifier or numeric literal) or single or double quoted, in either
|
|
* the `@param` or in source, we need to strip the quotes to give a fair
|
|
* comparison.
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
const dropPathSegmentQuotes = (str) => {
|
|
return str.replaceAll(/\.(['"])(.*)\1/gu, '.$2');
|
|
};
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @returns {(otherPathName: string) => boolean}
|
|
*/
|
|
const comparePaths = (name) => {
|
|
return (otherPathName) => {
|
|
return otherPathName === name ||
|
|
dropPathSegmentQuotes(otherPathName) === dropPathSegmentQuotes(name);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @callback PathDoesNotBeginWith
|
|
* @param {string} name
|
|
* @param {string} otherPathName
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
/** @type {PathDoesNotBeginWith} */
|
|
const pathDoesNotBeginWith = (name, otherPathName) => {
|
|
return !name.startsWith(otherPathName) &&
|
|
!dropPathSegmentQuotes(name).startsWith(dropPathSegmentQuotes(otherPathName));
|
|
};
|
|
|
|
/**
|
|
* @param {string} regexString
|
|
* @param {string} [requiredFlags]
|
|
* @returns {RegExp}
|
|
*/
|
|
const getRegexFromString = (regexString, requiredFlags) => {
|
|
const match = regexString.match(/^\/(.*)\/([gimyus]*)$/us);
|
|
let flags = 'u';
|
|
let regex = regexString;
|
|
if (match) {
|
|
[
|
|
, regex,
|
|
flags,
|
|
] = match;
|
|
if (!flags) {
|
|
flags = 'u';
|
|
}
|
|
}
|
|
|
|
const uniqueFlags = [
|
|
...new Set(flags + (requiredFlags || '')),
|
|
];
|
|
flags = uniqueFlags.join('');
|
|
|
|
return new RegExp(regex, flags);
|
|
};
|
|
|
|
export {
|
|
comparePaths,
|
|
dropPathSegmentQuotes,
|
|
enforcedContexts,
|
|
exemptSpeciaMethods,
|
|
filterTags,
|
|
flattenRoots,
|
|
forEachPreferredTag,
|
|
getAllTags,
|
|
getContextObject,
|
|
getFunctionParameterNames,
|
|
getIndent,
|
|
getJsdocTagsDeep,
|
|
getPreferredTagName,
|
|
getPreferredTagNameSimple,
|
|
getRegexFromString,
|
|
getTagDescription,
|
|
getTags,
|
|
getTagsByType,
|
|
getTagStructureForMode,
|
|
hasATag,
|
|
hasParams,
|
|
hasReturnValue,
|
|
hasTag,
|
|
hasThrowValue,
|
|
hasValueOrExecutorHasNonEmptyResolveValue,
|
|
hasYieldValue,
|
|
isConstructor,
|
|
isGetter,
|
|
isNamepathDefiningTag,
|
|
isNamepathOrUrlReferencingTag,
|
|
isNamepathReferencingTag,
|
|
isSetter,
|
|
isValidTag,
|
|
mayBeUndefinedTypeTag,
|
|
overrideTagStructure,
|
|
parseClosureTemplateTag,
|
|
pathDoesNotBeginWith,
|
|
setTagStructure,
|
|
tagMightHaveEitherTypeOrNamePosition,
|
|
tagMightHaveNamepath,
|
|
tagMightHaveNamePosition,
|
|
tagMightHaveTypePosition,
|
|
tagMissingRequiredTypeOrNamepath,
|
|
tagMustHaveNamePosition,
|
|
tagMustHaveTypePosition,
|
|
};
|