Fix actions/tour

This commit is contained in:
2025-02-13 23:19:32 +01:00
parent 47dd1adb30
commit fa21d30994
4543 changed files with 680810 additions and 0 deletions

34
node_modules/eslint-plugin-jsdoc/src/WarnSettings.js generated vendored Normal file
View File

@@ -0,0 +1,34 @@
const WarnSettings = function () {
/** @type {WeakMap<object, Set<string>>} */
const warnedSettings = new WeakMap();
return {
/**
* Warn only once for each context and setting
* @param {{}} context
* @param {string} setting
* @returns {boolean}
*/
hasBeenWarned (context, setting) {
return warnedSettings.has(context) && /** @type {Set<string>} */ (
warnedSettings.get(context)
).has(setting);
},
/**
* @param {{}} context
* @param {string} setting
* @returns {void}
*/
markSettingAsWarned (context, setting) {
// c8 ignore else
if (!warnedSettings.has(context)) {
warnedSettings.set(context, new Set());
}
/** @type {Set<string>} */ (warnedSettings.get(context)).add(setting);
},
};
};
export default WarnSettings;

358
node_modules/eslint-plugin-jsdoc/src/alignTransform.js generated vendored Normal file
View File

@@ -0,0 +1,358 @@
/**
* Transform based on https://github.com/syavorsky/comment-parser/blob/master/src/transforms/align.ts
*
* It contains some customizations to align based on the tags, and some custom options.
*/
import {
// `comment-parser/primitives` export
util,
} from 'comment-parser';
/**
* @typedef {{
* hasNoTypes: boolean,
* maxNamedTagLength: import('./iterateJsdoc.js').Integer,
* maxUnnamedTagLength: import('./iterateJsdoc.js').Integer
* }} TypelessInfo
*/
const {
rewireSource,
} = util;
/**
* @typedef {{
* name: import('./iterateJsdoc.js').Integer,
* start: import('./iterateJsdoc.js').Integer,
* tag: import('./iterateJsdoc.js').Integer,
* type: import('./iterateJsdoc.js').Integer
* }} Width
*/
/** @type {Width} */
const zeroWidth = {
name: 0,
start: 0,
tag: 0,
type: 0,
};
/**
* @param {string[]} tags
* @param {import('./iterateJsdoc.js').Integer} index
* @param {import('comment-parser').Line[]} source
* @returns {boolean}
*/
const shouldAlign = (tags, index, source) => {
const tag = source[index].tokens.tag.replace('@', '');
const includesTag = tags.includes(tag);
if (includesTag) {
return true;
}
if (tag !== '') {
return false;
}
for (let iterator = index; iterator >= 0; iterator--) {
const previousTag = source[iterator].tokens.tag.replace('@', '');
if (previousTag !== '') {
if (tags.includes(previousTag)) {
return true;
}
return false;
}
}
return true;
};
/**
* @param {string[]} tags
* @returns {(
* width: Width,
* line: {
* tokens: import('comment-parser').Tokens
* },
* index: import('./iterateJsdoc.js').Integer,
* source: import('comment-parser').Line[]
* ) => Width}
*/
const getWidth = (tags) => {
return (width, {
tokens,
}, index, source) => {
if (!shouldAlign(tags, index, source)) {
return width;
}
return {
name: Math.max(width.name, tokens.name.length),
start: tokens.delimiter === '/**' ? tokens.start.length : width.start,
tag: Math.max(width.tag, tokens.tag.length),
type: Math.max(width.type, tokens.type.length),
};
};
};
/**
* @param {{
* description: string;
* tags: import('comment-parser').Spec[];
* problems: import('comment-parser').Problem[];
* }} fields
* @returns {TypelessInfo}
*/
const getTypelessInfo = (fields) => {
const hasNoTypes = fields.tags.every(({
type,
}) => {
return !type;
});
const maxNamedTagLength = Math.max(...fields.tags.map(({
tag,
name,
}) => {
return name.length === 0 ? -1 : tag.length;
}).filter((length) => {
return length !== -1;
})) + 1;
const maxUnnamedTagLength = Math.max(...fields.tags.map(({
tag,
name,
}) => {
return name.length === 0 ? tag.length : -1;
}).filter((length) => {
return length !== -1;
})) + 1;
return {
hasNoTypes,
maxNamedTagLength,
maxUnnamedTagLength,
};
};
/**
* @param {import('./iterateJsdoc.js').Integer} len
* @returns {string}
*/
const space = (len) => {
return ''.padStart(len, ' ');
};
/**
* @param {{
* customSpacings: import('../src/rules/checkLineAlignment.js').CustomSpacings,
* tags: string[],
* indent: string,
* preserveMainDescriptionPostDelimiter: boolean,
* wrapIndent: string,
* disableWrapIndent: boolean,
* }} cfg
* @returns {(
* block: import('comment-parser').Block
* ) => import('comment-parser').Block}
*/
const alignTransform = ({
customSpacings,
tags,
indent,
preserveMainDescriptionPostDelimiter,
wrapIndent,
disableWrapIndent,
}) => {
let intoTags = false;
/** @type {Width} */
let width;
/**
* @param {import('comment-parser').Tokens} tokens
* @param {TypelessInfo} typelessInfo
* @returns {import('comment-parser').Tokens}
*/
const alignTokens = (tokens, typelessInfo) => {
const nothingAfter = {
delim: false,
name: false,
tag: false,
type: false,
};
if (tokens.description === '') {
nothingAfter.name = true;
tokens.postName = '';
if (tokens.name === '') {
nothingAfter.type = true;
tokens.postType = '';
if (tokens.type === '') {
nothingAfter.tag = true;
tokens.postTag = '';
/* c8 ignore next: Never happens because the !intoTags return. But it's here for consistency with the original align transform */
if (tokens.tag === '') {
nothingAfter.delim = true;
}
}
}
}
let untypedNameAdjustment = 0;
let untypedTypeAdjustment = 0;
if (typelessInfo.hasNoTypes) {
nothingAfter.tag = true;
tokens.postTag = '';
if (tokens.name === '') {
untypedNameAdjustment = typelessInfo.maxNamedTagLength - tokens.tag.length;
} else {
untypedNameAdjustment = typelessInfo.maxNamedTagLength > typelessInfo.maxUnnamedTagLength ? 0 :
Math.max(0, typelessInfo.maxUnnamedTagLength - (tokens.tag.length + tokens.name.length + 1));
untypedTypeAdjustment = typelessInfo.maxNamedTagLength - tokens.tag.length;
}
}
// Todo: Avoid fixing alignment of blocks with multiline wrapping of type
if (tokens.tag === '' && tokens.type) {
return tokens;
}
const spacings = {
postDelimiter: customSpacings?.postDelimiter || 1,
postName: customSpacings?.postName || 1,
postTag: customSpacings?.postTag || 1,
postType: customSpacings?.postType || 1,
};
tokens.postDelimiter = nothingAfter.delim ? '' : space(spacings.postDelimiter);
if (!nothingAfter.tag) {
tokens.postTag = space(width.tag - tokens.tag.length + spacings.postTag);
}
if (!nothingAfter.type) {
tokens.postType = space(width.type - tokens.type.length + spacings.postType + untypedTypeAdjustment);
}
if (!nothingAfter.name) {
// If post name is empty for all lines (name width 0), don't add post name spacing.
tokens.postName = width.name === 0 ? '' : space(width.name - tokens.name.length + spacings.postName + untypedNameAdjustment);
}
return tokens;
};
/**
* @param {import('comment-parser').Line} line
* @param {import('./iterateJsdoc.js').Integer} index
* @param {import('comment-parser').Line[]} source
* @param {TypelessInfo} typelessInfo
* @param {string|false} indentTag
* @returns {import('comment-parser').Line}
*/
const update = (line, index, source, typelessInfo, indentTag) => {
/** @type {import('comment-parser').Tokens} */
const tokens = {
...line.tokens,
};
if (tokens.tag !== '') {
intoTags = true;
}
const isEmpty =
tokens.tag === '' &&
tokens.name === '' &&
tokens.type === '' &&
tokens.description === '';
// dangling '*/'
if (tokens.end === '*/' && isEmpty) {
tokens.start = indent + ' ';
return {
...line,
tokens,
};
}
switch (tokens.delimiter) {
case '/**':
tokens.start = indent;
break;
case '*':
tokens.start = indent + ' ';
break;
default:
tokens.delimiter = '';
// compensate delimiter
tokens.start = indent + ' ';
}
if (!intoTags) {
if (tokens.description === '') {
tokens.postDelimiter = '';
} else if (!preserveMainDescriptionPostDelimiter) {
tokens.postDelimiter = ' ';
}
return {
...line,
tokens,
};
}
const postHyphenSpacing = customSpacings?.postHyphen ?? 1;
const hyphenSpacing = /^\s*-\s+/u;
tokens.description = tokens.description.replace(
hyphenSpacing, '-' + ''.padStart(postHyphenSpacing, ' '),
);
// Not align.
if (shouldAlign(tags, index, source)) {
alignTokens(tokens, typelessInfo);
if (!disableWrapIndent && indentTag) {
tokens.postDelimiter += wrapIndent;
}
}
return {
...line,
tokens,
};
};
return ({
source,
...fields
}) => {
width = source.reduce(getWidth(tags), {
...zeroWidth,
});
const typelessInfo = getTypelessInfo(fields);
let tagIndentMode = false;
return rewireSource({
...fields,
source: source.map((line, index) => {
const indentTag = !disableWrapIndent && tagIndentMode && !line.tokens.tag && line.tokens.description;
const ret = update(line, index, source, typelessInfo, indentTag);
if (!disableWrapIndent && line.tokens.tag) {
tagIndentMode = true;
}
return ret;
}),
});
};
};
export default alignTransform;

169
node_modules/eslint-plugin-jsdoc/src/defaultTagOrder.js generated vendored Normal file
View File

@@ -0,0 +1,169 @@
const defaultTagOrder = [
{
tags: [
// Brief descriptions
'summary',
'typeSummary',
// Module/file-level
'module',
'exports',
'file',
'fileoverview',
'overview',
'import',
// Identifying (name, type)
'template',
'typedef',
'interface',
'record',
'name',
'kind',
'type',
'alias',
'external',
'host',
'callback',
'func',
'function',
'method',
'class',
'constructor',
// Relationships
'modifies',
'mixes',
'mixin',
'mixinClass',
'mixinFunction',
'namespace',
'borrows',
'constructs',
'lends',
'implements',
'requires',
// Long descriptions
'desc',
'description',
'classdesc',
'tutorial',
'copyright',
'license',
// Simple annotations
// TypeScript
'internal',
'overload',
'const',
'constant',
'final',
'global',
'readonly',
'abstract',
'virtual',
'var',
'member',
'memberof',
'memberof!',
'inner',
'instance',
'inheritdoc',
'inheritDoc',
'override',
'hideconstructor',
// Core function/object info
'param',
'arg',
'argument',
'prop',
'property',
'return',
'returns',
// Important behavior details
'async',
'generator',
'default',
'defaultvalue',
'enum',
'augments',
'extends',
'throws',
'exception',
'yield',
'yields',
'event',
'fires',
'emits',
'listens',
'this',
// TypeScript
'satisfies',
// Access
'static',
'private',
'protected',
'public',
'access',
'package',
'-other',
// Supplementary descriptions
'see',
'example',
// METADATA
// Other Closure (undocumented) metadata
'closurePrimitive',
'customElement',
'expose',
'hidden',
'idGenerator',
'meaning',
'ngInject',
'owner',
'wizaction',
// Other Closure (documented) metadata
'define',
'dict',
'export',
'externs',
'implicitCast',
'noalias',
'nocollapse',
'nocompile',
'noinline',
'nosideeffects',
'polymer',
'polymerBehavior',
'preserve',
'struct',
'suppress',
'unrestricted',
// @homer0/prettier-plugin-jsdoc metadata
'category',
// Non-Closure metadata
'ignore',
'author',
'version',
'variation',
'since',
'deprecated',
'todo',
],
},
];
export default defaultTagOrder;

964
node_modules/eslint-plugin-jsdoc/src/exportParser.js generated vendored Normal file
View File

@@ -0,0 +1,964 @@
import {
findJSDocComment,
} from '@es-joy/jsdoccomment';
import debugModule from 'debug';
const debug = debugModule('requireExportJsdoc');
/**
* @typedef {{
* value: string
* }} ValueObject
*/
/**
* @typedef {{
* type?: string,
* value?: ValueObject|import('eslint').Rule.Node,
* props: {
* [key: string]: CreatedNode|null,
* },
* special?: true,
* globalVars?: CreatedNode,
* exported?: boolean,
* ANONYMOUS_DEFAULT?: import('eslint').Rule.Node
* }} CreatedNode
*/
/**
* @returns {CreatedNode}
*/
const createNode = function () {
return {
props: {},
};
};
/**
* @param {CreatedNode|null} symbol
* @returns {string|null}
*/
const getSymbolValue = function (symbol) {
/* c8 ignore next 3 */
if (!symbol) {
return null;
}
/* c8 ignore else */
if (symbol.type === 'literal') {
return /** @type {ValueObject} */ (symbol.value).value;
}
/* c8 ignore next */
return null;
};
/**
*
* @param {import('estree').Identifier} node
* @param {CreatedNode} globals
* @param {CreatedNode} scope
* @param {SymbolOptions} opts
* @returns {CreatedNode|null}
*/
const getIdentifier = function (node, globals, scope, opts) {
if (opts.simpleIdentifier) {
// Type is Identier for noncomputed properties
const identifierLiteral = createNode();
identifierLiteral.type = 'literal';
identifierLiteral.value = {
value: node.name,
};
return identifierLiteral;
}
/* c8 ignore next */
const block = scope || globals;
// As scopes are not currently supported, they are not traversed upwards recursively
if (block.props[node.name]) {
return block.props[node.name];
}
// Seems this will only be entered once scopes added and entered
/* c8 ignore next 3 */
if (globals.props[node.name]) {
return globals.props[node.name];
}
return null;
};
/**
* @callback CreateSymbol
* @param {import('eslint').Rule.Node|null} node
* @param {CreatedNode} globals
* @param {import('eslint').Rule.Node|null} value
* @param {CreatedNode} [scope]
* @param {boolean|SymbolOptions} [isGlobal]
* @returns {CreatedNode|null}
*/
/** @type {CreateSymbol} */
let createSymbol; // eslint-disable-line prefer-const
/* eslint-disable complexity -- Temporary */
/**
* @typedef {{
* simpleIdentifier?: boolean
* }} SymbolOptions
*/
/**
*
* @param {import('eslint').Rule.Node} node
* @param {CreatedNode} globals
* @param {CreatedNode} scope
* @param {SymbolOptions} [opt]
* @returns {CreatedNode|null}
*/
const getSymbol = function (node, globals, scope, opt) {
/* eslint-enable complexity -- Temporary */
const opts = opt || {};
/* c8 ignore next */
switch (node.type) {
case 'Identifier': {
return getIdentifier(node, globals, scope, opts);
}
case 'MemberExpression': {
const obj = getSymbol(
/** @type {import('eslint').Rule.Node} */
(node.object),
globals,
scope,
opts,
);
const propertySymbol = getSymbol(
/** @type {import('eslint').Rule.Node} */
(node.property),
globals,
scope,
{
simpleIdentifier: !node.computed,
},
);
const propertyValue = getSymbolValue(propertySymbol);
/* c8 ignore else */
if (obj && propertyValue && obj.props[propertyValue]) {
const block = obj.props[propertyValue];
return block;
}
/* c8 ignore next 10 */
/*
if (opts.createMissingProps && propertyValue) {
obj.props[propertyValue] = createNode();
return obj.props[propertyValue];
}
*/
debug(`MemberExpression: Missing property ${
/** @type {import('estree').PrivateIdentifier} */ (node.property).name
}`);
/* c8 ignore next 2 */
return null;
}
case 'ClassExpression': {
return getSymbol(
/** @type {import('eslint').Rule.Node} */
(node.body),
globals,
scope,
opts,
);
}
/* c8 ignore next 7 -- No longer needed? */
// @ts-expect-error TS OK
case 'TSTypeAliasDeclaration':
// @ts-expect-error TS OK
// Fallthrough
case 'TSEnumDeclaration':
// @ts-expect-error TS OK
case 'TSInterfaceDeclaration':
case 'ClassDeclaration':
case 'FunctionExpression': case 'FunctionDeclaration':
case 'ArrowFunctionExpression': {
const val = createNode();
val.props.prototype = createNode();
val.props.prototype.type = 'object';
val.type = 'object';
val.value = node;
return val;
}
case 'AssignmentExpression': {
return createSymbol(
/** @type {import('eslint').Rule.Node} */
(node.left),
globals,
/** @type {import('eslint').Rule.Node} */
(node.right),
scope,
opts,
);
}
case 'ClassBody': {
const val = createNode();
for (const method of node.body) {
val.props[
/** @type {import('estree').Identifier} */ (
/** @type {import('estree').MethodDefinition} */ (
method
).key
).name
] = createNode();
/** @type {{[key: string]: CreatedNode}} */ (val.props)[
/** @type {import('estree').Identifier} */ (
/** @type {import('estree').MethodDefinition} */ (
method
).key
).name
].type = 'object';
/** @type {{[key: string]: CreatedNode}} */ (val.props)[
/** @type {import('estree').Identifier} */ (
/** @type {import('estree').MethodDefinition} */ (
method
).key
).name
].value = /** @type {import('eslint').Rule.Node} */ (
/** @type {import('estree').MethodDefinition} */ (method).value
);
}
val.type = 'object';
val.value = node.parent;
return val;
}
case 'ObjectExpression': {
const val = createNode();
val.type = 'object';
for (const prop of node.properties) {
if ([
// typescript-eslint, espree, acorn, etc.
'SpreadElement',
// @babel/eslint-parser
'ExperimentalSpreadProperty',
].includes(prop.type)) {
continue;
}
const propVal = getSymbol(
/** @type {import('eslint').Rule.Node} */ (
/** @type {import('estree').Property} */
(prop).value
),
globals,
scope,
opts,
);
/* c8 ignore next 8 */
if (propVal) {
val.props[
/** @type {import('estree').PrivateIdentifier} */
(
/** @type {import('estree').Property} */ (prop).key
).name
] = propVal;
}
}
return val;
}
case 'Literal': {
const val = createNode();
val.type = 'literal';
val.value = node;
return val;
}
}
/* c8 ignore next */
return null;
};
/**
*
* @param {CreatedNode} block
* @param {string} name
* @param {CreatedNode|null} value
* @param {CreatedNode} globals
* @param {boolean|SymbolOptions|undefined} isGlobal
* @returns {void}
*/
const createBlockSymbol = function (block, name, value, globals, isGlobal) {
block.props[name] = value;
if (isGlobal && globals.props.window && globals.props.window.special) {
globals.props.window.props[name] = value;
}
};
createSymbol = function (node, globals, value, scope, isGlobal) {
const block = scope || globals;
/* c8 ignore next 3 */
if (!node) {
return null;
}
let symbol;
switch (node.type) {
case 'FunctionDeclaration':
/* c8 ignore next */
// @ts-expect-error TS OK
// Fall through
case 'TSEnumDeclaration': case 'TSInterfaceDeclaration':
/* c8 ignore next */
// @ts-expect-error TS OK
// Fall through
case 'TSTypeAliasDeclaration': case 'ClassDeclaration': {
const nde = /** @type {import('estree').ClassDeclaration} */ (node);
/* c8 ignore else */
if (nde.id && nde.id.type === 'Identifier') {
return createSymbol(
/** @type {import('eslint').Rule.Node} */ (nde.id),
globals,
node,
globals,
);
}
/* c8 ignore next 2 */
break;
}
case 'Identifier': {
const nde = /** @type {import('estree').Identifier} */ (node);
if (value) {
const valueSymbol = getSymbol(value, globals, block);
/* c8 ignore else */
if (valueSymbol) {
createBlockSymbol(block, nde.name, valueSymbol, globals, isGlobal);
return block.props[nde.name];
}
/* c8 ignore next */
debug('Identifier: Missing value symbol for %s', nde.name);
} else {
createBlockSymbol(block, nde.name, createNode(), globals, isGlobal);
return block.props[nde.name];
}
/* c8 ignore next 2 */
break;
}
case 'MemberExpression': {
const nde = /** @type {import('estree').MemberExpression} */ (node);
symbol = getSymbol(
/** @type {import('eslint').Rule.Node} */ (nde.object), globals, block,
);
const propertySymbol = getSymbol(
/** @type {import('eslint').Rule.Node} */ (nde.property),
globals,
block,
{
simpleIdentifier: !nde.computed,
},
);
const propertyValue = getSymbolValue(propertySymbol);
if (symbol && propertyValue) {
createBlockSymbol(symbol, propertyValue, getSymbol(
/** @type {import('eslint').Rule.Node} */
(value), globals, block,
), globals, isGlobal);
return symbol.props[propertyValue];
}
debug(
'MemberExpression: Missing symbol: %s',
/** @type {import('estree').Identifier} */ (
nde.property
).name,
);
break;
}
}
return null;
};
/**
* Creates variables from variable definitions
* @param {import('eslint').Rule.Node} node
* @param {CreatedNode} globals
* @param {import('./rules/requireJsdoc.js').RequireJsdocOpts} opts
* @returns {void}
*/
const initVariables = function (node, globals, opts) {
switch (node.type) {
case 'Program': {
for (const childNode of node.body) {
initVariables(
/** @type {import('eslint').Rule.Node} */
(childNode),
globals,
opts,
);
}
break;
}
case 'ExpressionStatement': {
initVariables(
/** @type {import('eslint').Rule.Node} */
(node.expression),
globals,
opts,
);
break;
}
case 'VariableDeclaration': {
for (const declaration of node.declarations) {
// let and const
const symbol = createSymbol(
/** @type {import('eslint').Rule.Node} */
(declaration.id),
globals,
null,
globals,
);
if (opts.initWindow && node.kind === 'var' && globals.props.window) {
// If var, also add to window
globals.props.window.props[
/** @type {import('estree').Identifier} */
(declaration.id).name
] = symbol;
}
}
break;
}
case 'ExportNamedDeclaration': {
if (node.declaration) {
initVariables(
/** @type {import('eslint').Rule.Node} */
(node.declaration),
globals,
opts,
);
}
break;
}
}
};
/* eslint-disable complexity -- Temporary */
/**
* Populates variable maps using AST
* @param {import('eslint').Rule.Node} node
* @param {CreatedNode} globals
* @param {import('./rules/requireJsdoc.js').RequireJsdocOpts} opt
* @param {true} [isExport]
* @returns {boolean}
*/
const mapVariables = function (node, globals, opt, isExport) {
/* eslint-enable complexity -- Temporary */
/* c8 ignore next */
const opts = opt || {};
/* c8 ignore next */
switch (node.type) {
case 'Program': {
if (opts.ancestorsOnly) {
return false;
}
for (const childNode of node.body) {
mapVariables(
/** @type {import('eslint').Rule.Node} */
(childNode),
globals,
opts,
);
}
break;
}
case 'ExpressionStatement': {
mapVariables(
/** @type {import('eslint').Rule.Node} */
(node.expression),
globals,
opts,
);
break;
}
case 'AssignmentExpression': {
createSymbol(
/** @type {import('eslint').Rule.Node} */
(node.left),
globals,
/** @type {import('eslint').Rule.Node} */
(node.right),
);
break;
}
case 'VariableDeclaration': {
for (const declaration of node.declarations) {
const isGlobal = Boolean(opts.initWindow && node.kind === 'var' && globals.props.window);
const symbol = createSymbol(
/** @type {import('eslint').Rule.Node} */
(declaration.id),
globals,
/** @type {import('eslint').Rule.Node} */
(declaration.init),
globals,
isGlobal,
);
if (symbol && isExport) {
symbol.exported = true;
}
}
break;
}
case 'FunctionDeclaration': {
/* c8 ignore next 10 */
if (/** @type {import('estree').Identifier} */ (node.id).type === 'Identifier') {
createSymbol(
/** @type {import('eslint').Rule.Node} */
(node.id),
globals,
node,
globals,
true,
);
}
break;
}
case 'ExportDefaultDeclaration': {
const symbol = createSymbol(
/** @type {import('eslint').Rule.Node} */
(node.declaration),
globals,
/** @type {import('eslint').Rule.Node} */
(node.declaration),
);
if (symbol) {
symbol.exported = true;
/* c8 ignore next 6 */
} else {
// if (!node.id) {
globals.ANONYMOUS_DEFAULT = /** @type {import('eslint').Rule.Node} */ (
node.declaration
);
}
break;
}
case 'ExportNamedDeclaration': {
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
mapVariables(
/** @type {import('eslint').Rule.Node} */
(node.declaration),
globals,
opts,
true,
);
} else {
const symbol = createSymbol(
/** @type {import('eslint').Rule.Node} */
(node.declaration),
globals,
/** @type {import('eslint').Rule.Node} */
(node.declaration),
);
/* c8 ignore next 3 */
if (symbol) {
symbol.exported = true;
}
}
}
for (const specifier of node.specifiers) {
mapVariables(
/** @type {import('eslint').Rule.Node} */
(specifier),
globals,
opts,
);
}
break;
}
case 'ExportSpecifier': {
const symbol = getSymbol(
/** @type {import('eslint').Rule.Node} */
(node.local),
globals,
globals,
);
/* c8 ignore next 3 */
if (symbol) {
symbol.exported = true;
}
break;
}
case 'ClassDeclaration': {
createSymbol(
/** @type {import('eslint').Rule.Node|null} */ (node.id),
globals,
/** @type {import('eslint').Rule.Node} */ (node.body),
globals,
);
break;
}
default: {
/* c8 ignore next */
return false;
}
}
return true;
};
/**
*
* @param {import('eslint').Rule.Node} node
* @param {CreatedNode|ValueObject|string|undefined|
* import('eslint').Rule.Node} block
* @param {(CreatedNode|ValueObject|string|
* import('eslint').Rule.Node)[]} [cache]
* @returns {boolean}
*/
const findNode = function (node, block, cache) {
let blockCache = cache || [];
if (!block || blockCache.includes(block)) {
return false;
}
blockCache = blockCache.slice();
blockCache.push(block);
if (
typeof block === 'object' &&
'type' in block &&
(block.type === 'object' || block.type === 'MethodDefinition') &&
block.value === node
) {
return true;
}
if (typeof block !== 'object') {
return false;
}
const props = ('props' in block && block.props) || ('body' in block && block.body);
for (const propval of Object.values(props || {})) {
if (Array.isArray(propval)) {
/* c8 ignore next 5 */
if (propval.some((val) => {
return findNode(node, val, blockCache);
})) {
return true;
}
} else if (findNode(node, propval, blockCache)) {
return true;
}
}
return false;
};
const exportTypes = new Set([
'ExportNamedDeclaration', 'ExportDefaultDeclaration',
]);
const ignorableNestedTypes = new Set([
'FunctionDeclaration', 'ArrowFunctionExpression', 'FunctionExpression',
]);
/**
* @param {import('eslint').Rule.Node} nde
* @returns {import('eslint').Rule.Node|false}
*/
const getExportAncestor = function (nde) {
let node = nde;
let idx = 0;
const ignorableIfDeep = ignorableNestedTypes.has(nde?.type);
while (node) {
// Ignore functions nested more deeply than say `export default function () {}`
if (idx >= 2 && ignorableIfDeep) {
break;
}
if (exportTypes.has(node.type)) {
return node;
}
node = node.parent;
idx++;
}
return false;
};
const canBeExportedByAncestorType = new Set([
'TSPropertySignature',
'TSMethodSignature',
'ClassProperty',
'PropertyDefinition',
'Method',
]);
const canExportChildrenType = new Set([
'TSInterfaceBody',
'TSInterfaceDeclaration',
'TSTypeLiteral',
'TSTypeAliasDeclaration',
'TSTypeParameterInstantiation',
'TSTypeReference',
'ClassDeclaration',
'ClassBody',
'ClassDefinition',
'ClassExpression',
'Program',
]);
/**
* @param {import('eslint').Rule.Node} nde
* @returns {false|import('eslint').Rule.Node}
*/
const isExportByAncestor = function (nde) {
if (!canBeExportedByAncestorType.has(nde.type)) {
return false;
}
let node = nde.parent;
while (node) {
if (exportTypes.has(node.type)) {
return node;
}
if (!canExportChildrenType.has(node.type)) {
return false;
}
node = node.parent;
}
return false;
};
/**
*
* @param {CreatedNode} block
* @param {import('eslint').Rule.Node} node
* @param {CreatedNode[]} [cache] Currently unused
* @returns {boolean}
*/
const findExportedNode = function (block, node, cache) {
/* c8 ignore next 3 */
if (block === null) {
return false;
}
const blockCache = cache || [];
const {
props,
} = block;
for (const propval of Object.values(props)) {
const pval = /** @type {CreatedNode} */ (propval);
blockCache.push(pval);
if (pval.exported && (node === pval.value || findNode(node, pval.value))) {
return true;
}
// No need to check `propval` for exported nodes as ESM
// exports are only global
}
return false;
};
/**
*
* @param {import('eslint').Rule.Node} node
* @param {CreatedNode} globals
* @param {import('./rules/requireJsdoc.js').RequireJsdocOpts} opt
* @returns {boolean}
*/
const isNodeExported = function (node, globals, opt) {
const moduleExports = globals.props.module?.props?.exports;
if (
opt.initModuleExports && moduleExports && findNode(node, moduleExports)
) {
return true;
}
if (opt.initWindow && globals.props.window && findNode(node, globals.props.window)) {
return true;
}
if (opt.esm && findExportedNode(globals, node)) {
return true;
}
return false;
};
/**
*
* @param {import('eslint').Rule.Node} node
* @param {CreatedNode} globalVars
* @param {import('./rules/requireJsdoc.js').RequireJsdocOpts} opts
* @returns {boolean}
*/
const parseRecursive = function (node, globalVars, opts) {
// Iterate from top using recursion - stop at first processed node from top
if (node.parent && parseRecursive(node.parent, globalVars, opts)) {
return true;
}
return mapVariables(node, globalVars, opts);
};
/**
*
* @param {import('eslint').Rule.Node} ast
* @param {import('eslint').Rule.Node} node
* @param {import('./rules/requireJsdoc.js').RequireJsdocOpts} opt
* @returns {CreatedNode}
*/
const parse = function (ast, node, opt) {
/* c8 ignore next 6 */
const opts = opt || {
ancestorsOnly: false,
esm: true,
initModuleExports: true,
initWindow: true,
};
const globalVars = createNode();
if (opts.initModuleExports) {
globalVars.props.module = createNode();
globalVars.props.module.props.exports = createNode();
globalVars.props.exports = globalVars.props.module.props.exports;
}
if (opts.initWindow) {
globalVars.props.window = createNode();
globalVars.props.window.special = true;
}
if (opts.ancestorsOnly) {
parseRecursive(node, globalVars, opts);
} else {
initVariables(ast, globalVars, opts);
mapVariables(ast, globalVars, opts);
}
return {
globalVars,
props: {},
};
};
const accessibilityNodes = new Set([
'PropertyDefinition',
'MethodDefinition',
]);
/**
*
* @param {import('eslint').Rule.Node} node
* @returns {boolean}
*/
const isPrivate = (node) => {
return accessibilityNodes.has(node.type) &&
(
'accessibility' in node &&
node.accessibility !== 'public' && node.accessibility !== undefined
) ||
'key' in node &&
node.key.type === 'PrivateIdentifier';
};
/**
*
* @param {import('eslint').Rule.Node} node
* @param {import('eslint').SourceCode} sourceCode
* @param {import('./rules/requireJsdoc.js').RequireJsdocOpts} opt
* @param {import('./iterateJsdoc.js').Settings} settings
* @returns {boolean}
*/
const isUncommentedExport = function (node, sourceCode, opt, settings) {
// console.log({node});
// Optimize with ancestor check for esm
if (opt.esm) {
if (isPrivate(node) ||
node.parent && isPrivate(node.parent)) {
return false;
}
const exportNode = getExportAncestor(node);
// Is export node comment
if (exportNode && !findJSDocComment(exportNode, sourceCode, settings)) {
return true;
}
/**
* Some typescript types are not in variable map, but inherit exported (interface property and method)
*/
if (
isExportByAncestor(node) &&
!findJSDocComment(node, sourceCode, settings)
) {
return true;
}
}
const ast = /** @type {unknown} */ (sourceCode.ast);
const parseResult = parse(
/** @type {import('eslint').Rule.Node} */
(ast),
node,
opt,
);
return isNodeExported(
node, /** @type {CreatedNode} */ (parseResult.globalVars), opt,
);
};
export default {
isUncommentedExport,
parse,
};

View File

@@ -0,0 +1,969 @@
/**
* @typedef {Map<string, Map<string, (string|boolean)>>} TagStructure
*/
/**
* @param {import('./jsdocUtils.js').ParserMode} mode
* @returns {TagStructure}
*/
const getDefaultTagStructureForMode = (mode) => {
const isJsdoc = mode === 'jsdoc';
const isClosure = mode === 'closure';
const isTypescript = mode === 'typescript';
const isPermissive = mode === 'permissive';
const isJsdocOrPermissive = isJsdoc || isPermissive;
const isJsdocOrTypescript = isJsdoc || isTypescript;
const isTypescriptOrClosure = isTypescript || isClosure;
const isClosureOrPermissive = isClosure || isPermissive;
const isJsdocTypescriptOrPermissive = isJsdocOrTypescript || isPermissive;
// Properties:
// `namepathRole` - 'namepath-referencing'|'namepath-defining'|'namepath-or-url-referencing'|'text'|false
// `typeAllowed` - boolean
// `nameRequired` - boolean
// `typeRequired` - boolean
// `typeOrNameRequired` - boolean
// All of `typeAllowed` have a signature with "type" except for
// `augments`/`extends` ("namepath")
// `param`/`arg`/`argument` (no signature)
// `property`/`prop` (no signature)
// `modifies` (undocumented)
// None of the `namepathRole: 'namepath-defining'` show as having curly
// brackets for their name/namepath
// Among `namepath-defining` and `namepath-referencing`, these do not seem
// to allow curly brackets in their doc signature or examples (`modifies`
// references namepaths within its type brackets and `param` is
// name-defining but not namepath-defining, so not part of these groups)
// Todo: Should support special processing for "name" as distinct from
// "namepath" (e.g., param can't define a namepath)
// Todo: Should support a `tutorialID` type (for `@tutorial` block and
// inline)
/**
* @type {TagStructure}
*/
return new Map([
[
'alias', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "namepath" (and no counter-examples)
[
'namepathRole', 'namepath-defining',
],
// "namepath"
[
'typeOrNameRequired', true,
],
])),
],
[
'arg', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', 'namepath-defining',
],
// See `param`
[
'nameRequired', true,
],
// Has no formal signature in the docs but shows curly brackets
// in the examples
[
'typeAllowed', true,
],
])),
],
[
'argument', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', 'namepath-defining',
],
// See `param`
[
'nameRequired', true,
],
// Has no formal signature in the docs but shows curly brackets
// in the examples
[
'typeAllowed', true,
],
])),
],
[
'augments', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "namepath" (and no counter-examples)
[
'namepathRole', 'namepath-referencing',
],
// Does not show curly brackets in either the signature or examples
[
'typeAllowed', true,
],
// "namepath"
[
'typeOrNameRequired', true,
],
])),
],
[
'borrows', new Map(/** @type {[string, string|boolean][]} */ ([
// `borrows` has a different format, however, so needs special parsing;
// seems to require both, and as "namepath"'s
[
'namepathRole', 'namepath-referencing',
],
// "namepath"
[
'typeOrNameRequired', true,
],
])),
],
[
'callback', new Map(/** @type {[string, string|boolean][]} */ ([
// Seems to require a "namepath" in the signature (with no
// counter-examples); TypeScript does not enforce but seems
// problematic as not attached so presumably not useable without it
[
'namepathRole', 'namepath-defining',
],
// "namepath"
[
'nameRequired', true,
],
])),
],
[
'class', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
// Not in use, but should be this value if using to power `empty-tags`
[
'nameAllowed', true,
],
[
'typeAllowed', true,
],
])),
],
[
'const', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
[
'typeAllowed', true,
],
])),
],
[
'constant', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
[
'typeAllowed', true,
],
])),
],
[
'constructor', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
[
'typeAllowed', true,
],
])),
],
[
'constructs', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
[
'nameRequired', false,
],
[
'typeAllowed', false,
],
])),
],
[
'define', new Map(/** @type {[string, string|boolean][]} */ ([
[
'typeRequired', isClosure,
],
])),
],
[
'emits', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "name" (of an event) and no counter-examples
[
'namepathRole', 'namepath-referencing',
],
[
'nameRequired', true,
],
[
'typeAllowed', false,
],
])),
],
[
'enum', new Map(/** @type {[string, string|boolean][]} */ ([
// Has example showing curly brackets but not in doc signature
[
'typeAllowed', true,
],
])),
],
[
'event', new Map(/** @type {[string, string|boolean][]} */ ([
// The doc signature of `event` seems to require a "name"
[
'nameRequired', true,
],
// Appears to require a "name" in its signature, albeit somewhat
// different from other "name"'s (including as described
// at https://jsdoc.app/about-namepaths.html )
[
'namepathRole', 'namepath-defining',
],
])),
],
[
'exception', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the signature and in the examples
[
'typeAllowed', true,
],
])),
],
// Closure
[
'export', new Map(/** @type {[string, string|boolean][]} */ ([
[
'typeAllowed', isClosureOrPermissive,
],
])),
],
[
'exports', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', 'namepath-defining',
],
[
'nameRequired', isJsdoc,
],
[
'typeAllowed', isClosureOrPermissive,
],
])),
],
[
'extends', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "namepath" (and no counter-examples)
[
'namepathRole', 'namepath-referencing',
],
// Does not show curly brackets in either the signature or examples
[
'typeAllowed', isTypescriptOrClosure || isPermissive,
],
[
'nameRequired', isJsdoc,
],
// "namepath"
[
'typeOrNameRequired', isTypescriptOrClosure || isPermissive,
],
])),
],
[
'external', new Map(/** @type {[string, string|boolean][]} */ ([
// Appears to require a "name" in its signature, albeit somewhat
// different from other "name"'s (including as described
// at https://jsdoc.app/about-namepaths.html )
[
'namepathRole', 'namepath-defining',
],
// "name" (and a special syntax for the `external` name)
[
'nameRequired', true,
],
[
'typeAllowed', false,
],
])),
],
[
'fires', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "name" (of an event) and no
// counter-examples
[
'namepathRole', 'namepath-referencing',
],
[
'nameRequired', true,
],
[
'typeAllowed', false,
],
])),
],
[
'function', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
[
'nameRequired', false,
],
[
'typeAllowed', false,
],
])),
],
[
'func', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
])),
],
[
'host', new Map(/** @type {[string, string|boolean][]} */ ([
// Appears to require a "name" in its signature, albeit somewhat
// different from other "name"'s (including as described
// at https://jsdoc.app/about-namepaths.html )
[
'namepathRole', 'namepath-defining',
],
// See `external`
[
'nameRequired', true,
],
[
'typeAllowed', false,
],
])),
],
[
'interface', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name" in signature, but indicates as optional
[
'namepathRole',
isJsdocTypescriptOrPermissive ? 'namepath-defining' : false,
],
// Not in use, but should be this value if using to power `empty-tags`
[
'nameAllowed', isClosure,
],
[
'typeAllowed', false,
],
])),
],
[
'internal', new Map(/** @type {[string, string|boolean][]} */ ([
// https://www.typescriptlang.org/tsconfig/#stripInternal
[
'namepathRole', false,
],
// Not in use, but should be this value if using to power `empty-tags`
[
'nameAllowed', false,
],
])),
],
[
'implements', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the doc signature and examples
// "typeExpression"
[
'typeRequired', true,
],
])),
],
[
'lends', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "namepath" (and no counter-examples)
[
'namepathRole', 'namepath-referencing',
],
// "namepath"
[
'typeOrNameRequired', true,
],
])),
],
[
'link', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a namepath OR URL and might be checked as such.
[
'namepathRole', 'namepath-or-url-referencing',
],
])),
],
[
'linkcode', new Map(/** @type {[string, string|boolean][]} */ ([
// Synonym for "link"
// Signature seems to require a namepath OR URL and might be checked as such.
[
'namepathRole', 'namepath-or-url-referencing',
],
])),
],
[
'linkplain', new Map(/** @type {[string, string|boolean][]} */ ([
// Synonym for "link"
// Signature seems to require a namepath OR URL and might be checked as such.
[
'namepathRole', 'namepath-or-url-referencing',
],
])),
],
[
'listens', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "name" (of an event) and no
// counter-examples
[
'namepathRole', 'namepath-referencing',
],
[
'nameRequired', true,
],
[
'typeAllowed', false,
],
])),
],
[
'member', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
// Has example showing curly brackets but not in doc signature
[
'typeAllowed', true,
],
])),
],
[
'memberof', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "namepath" (and no counter-examples),
// though it allows an incomplete namepath ending with connecting symbol
[
'namepathRole', 'namepath-referencing',
],
// "namepath"
[
'typeOrNameRequired', true,
],
])),
],
[
'memberof!', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "namepath" (and no counter-examples),
// though it allows an incomplete namepath ending with connecting symbol
[
'namepathRole', 'namepath-referencing',
],
// "namepath"
[
'typeOrNameRequired', true,
],
])),
],
[
'method', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
])),
],
[
'mixes', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "OtherObjectPath" with no
// counter-examples
[
'namepathRole', 'namepath-referencing',
],
// "OtherObjectPath"
[
'typeOrNameRequired', true,
],
])),
],
[
'mixin', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
[
'nameRequired', false,
],
[
'typeAllowed', false,
],
])),
],
[
'modifies', new Map(/** @type {[string, string|boolean][]} */ ([
// Has no documentation, but test example has curly brackets, and
// "name" would be suggested rather than "namepath" based on example;
// not sure if name is required
[
'typeAllowed', true,
],
])),
],
[
'module', new Map(/** @type {[string, string|boolean][]} */ ([
// Optional "name" and no curly brackets
// this block impacts `no-undefined-types` and `valid-types` (search for
// "isNamepathDefiningTag|tagMightHaveNamepath|tagMightHaveEitherTypeOrNamePosition")
[
'namepathRole', isJsdoc ? 'namepath-defining' : 'text',
],
// Shows the signature with curly brackets but not in the example
[
'typeAllowed', true,
],
])),
],
[
'name', new Map(/** @type {[string, string|boolean][]} */ ([
// Seems to require a "namepath" in the signature (with no
// counter-examples)
[
'namepathRole', 'namepath-defining',
],
// "namepath"
[
'nameRequired', true,
],
// "namepath"
[
'typeOrNameRequired', true,
],
])),
],
[
'namespace', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
// Shows the signature with curly brackets but not in the example
[
'typeAllowed', true,
],
])),
],
[
'package', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows the signature with curly brackets but not in the example
// "typeExpression"
[
'typeAllowed', isClosureOrPermissive,
],
])),
],
[
'param', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', 'namepath-defining',
],
// Though no signature provided requiring, per
// https://jsdoc.app/tags-param.html:
// "The @param tag requires you to specify the name of the parameter you
// are documenting."
[
'nameRequired', true,
],
// Has no formal signature in the docs but shows curly brackets
// in the examples
[
'typeAllowed', true,
],
])),
],
[
'private', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows the signature with curly brackets but not in the example
// "typeExpression"
[
'typeAllowed', isClosureOrPermissive,
],
])),
],
[
'prop', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', 'namepath-defining',
],
// See `property`
[
'nameRequired', true,
],
// Has no formal signature in the docs but shows curly brackets
// in the examples
[
'typeAllowed', true,
],
])),
],
[
'property', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', 'namepath-defining',
],
// No docs indicate required, but since parallel to `param`, we treat as
// such:
[
'nameRequired', true,
],
// Has no formal signature in the docs but shows curly brackets
// in the examples
[
'typeAllowed', true,
],
])),
],
[
'protected', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows the signature with curly brackets but not in the example
// "typeExpression"
[
'typeAllowed', isClosureOrPermissive,
],
])),
],
[
'public', new Map(/** @type {[string, string|boolean][]} */ ([
// Does not show a signature nor show curly brackets in the example
[
'typeAllowed', isClosureOrPermissive,
],
])),
],
[
'requires', new Map(/** @type {[string, string|boolean][]} */ ([
// <someModuleName>
[
'namepathRole', 'namepath-referencing',
],
[
'nameRequired', true,
],
[
'typeAllowed', false,
],
])),
],
[
'returns', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the signature and in the examples
[
'typeAllowed', true,
],
])),
],
[
'return', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the signature and in the examples
[
'typeAllowed', true,
],
])),
],
[
'satisfies', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the doc signature and examples
[
'typeRequired', true,
],
])),
],
[
'see', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature allows for "namepath" or text, so user must configure to
// 'namepath-referencing' to enforce checks
[
'namepathRole', 'text',
],
])),
],
[
'static', new Map(/** @type {[string, string|boolean][]} */ ([
// Does not show a signature nor show curly brackets in the example
[
'typeAllowed', isClosureOrPermissive,
],
])),
],
[
'suppress', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', !isClosure,
],
[
'typeRequired', isClosure,
],
])),
],
[
'template', new Map(/** @type {[string, string|boolean][]} */ ([
[
'namepathRole', isJsdoc ? 'text' : 'namepath-referencing',
],
[
'nameRequired', !isJsdoc,
],
// Though defines `namepathRole: 'namepath-defining'` in a sense, it is
// not parseable in the same way for template (e.g., allowing commas),
// so not adding
[
'typeAllowed', isTypescriptOrClosure || isPermissive,
],
])),
],
[
'this', new Map(/** @type {[string, string|boolean][]} */ ([
// Signature seems to require a "namepath" (and no counter-examples)
// Not used with namepath in Closure/TypeScript, however
[
'namepathRole', isJsdoc ? 'namepath-referencing' : false,
],
[
'typeRequired', isTypescriptOrClosure,
],
// namepath
[
'typeOrNameRequired', isJsdoc,
],
])),
],
[
'throws', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the signature and in the examples
[
'typeAllowed', true,
],
])),
],
[
'tutorial', new Map(/** @type {[string, string|boolean][]} */ ([
// (a tutorial ID)
[
'nameRequired', true,
],
[
'typeAllowed', false,
],
])),
],
[
'type', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the doc signature and examples
// "typeName"
[
'typeRequired', true,
],
])),
],
[
'typedef', new Map(/** @type {[string, string|boolean][]} */ ([
// Seems to require a "namepath" in the signature (with no
// counter-examples)
[
'namepathRole', 'namepath-defining',
],
// TypeScript may allow it to be dropped if followed by @property or @member;
// also shown as missing in Closure
// "namepath"
[
'nameRequired', isJsdocOrPermissive,
],
// Is not `typeRequired` for TypeScript because it gives an error:
// JSDoc '@typedef' tag should either have a type annotation or be followed by '@property' or '@member' tags.
// Has example showing curly brackets but not in doc signature
[
'typeAllowed', true,
],
// TypeScript may allow it to be dropped if followed by @property or @member
// "namepath"
[
'typeOrNameRequired', !isTypescript,
],
])),
],
[
'var', new Map(/** @type {[string, string|boolean][]} */ ([
// Allows for "name"'s in signature, but indicated as optional
[
'namepathRole', 'namepath-defining',
],
// Has example showing curly brackets but not in doc signature
[
'typeAllowed', true,
],
])),
],
[
'yields', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the signature and in the examples
[
'typeAllowed', true,
],
])),
],
[
'yield', new Map(/** @type {[string, string|boolean][]} */ ([
// Shows curly brackets in the signature and in the examples
[
'typeAllowed', true,
],
])),
],
]);
};
export default getDefaultTagStructureForMode;

View File

@@ -0,0 +1,606 @@
// Todo: Support TS by fenced block type
import {readFileSync} from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as espree from 'espree';
import {
getRegexFromString,
forEachPreferredTag,
getTagDescription,
getPreferredTagName,
hasTag,
} from './jsdocUtils.js';
import {
parseComment,
} from '@es-joy/jsdoccomment';
const __dirname = dirname(fileURLToPath(import.meta.url));
const {version} = JSON.parse(
// @ts-expect-error `Buffer` is ok for `JSON.parse`
readFileSync(join(__dirname, '../package.json'))
);
// const zeroBasedLineIndexAdjust = -1;
const likelyNestedJSDocIndentSpace = 1;
const preTagSpaceLength = 1;
// If a space is present, we should ignore it
const firstLinePrefixLength = preTagSpaceLength;
const hasCaptionRegex = /^\s*<caption>([\s\S]*?)<\/caption>/u;
/**
* @param {string} str
* @returns {string}
*/
const escapeStringRegexp = (str) => {
return str.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
};
/**
* @param {string} str
* @param {string} ch
* @returns {import('./iterateJsdoc.js').Integer}
*/
const countChars = (str, ch) => {
return (str.match(new RegExp(escapeStringRegexp(ch), 'gu')) || []).length;
};
/**
* @param {string} text
* @returns {[
* import('./iterateJsdoc.js').Integer,
* import('./iterateJsdoc.js').Integer
* ]}
*/
const getLinesCols = (text) => {
const matchLines = countChars(text, '\n');
const colDelta = matchLines ?
text.slice(text.lastIndexOf('\n') + 1).length :
text.length;
return [
matchLines, colDelta,
];
};
/**
* @typedef {number} Integer
*/
/**
* @typedef {object} JsdocProcessorOptions
* @property {boolean} [captionRequired]
* @property {Integer} [paddedIndent]
* @property {boolean} [checkDefaults]
* @property {boolean} [checkParams]
* @property {boolean} [checkExamples]
* @property {boolean} [checkProperties]
* @property {string} [matchingFileName]
* @property {string} [matchingFileNameDefaults]
* @property {string} [matchingFileNameParams]
* @property {string} [matchingFileNameProperties]
* @property {string} [exampleCodeRegex]
* @property {string} [rejectExampleCodeRegex]
* @property {"script"|"module"} [sourceType]
* @property {import('eslint').Linter.FlatConfigParserModule} [parser]
*/
/**
* We use a function for the ability of the user to pass in a config, but
* without requiring all users of the plugin to do so.
* @param {JsdocProcessorOptions} [options]
*/
export const getJsdocProcessorPlugin = (options = {}) => {
const {
exampleCodeRegex = null,
rejectExampleCodeRegex = null,
checkExamples = true,
checkDefaults = false,
checkParams = false,
checkProperties = false,
matchingFileName = null,
matchingFileNameDefaults = null,
matchingFileNameParams = null,
matchingFileNameProperties = null,
paddedIndent = 0,
captionRequired = false,
sourceType = 'module',
parser = undefined
} = options;
/** @type {RegExp} */
let exampleCodeRegExp;
/** @type {RegExp} */
let rejectExampleCodeRegExp;
if (exampleCodeRegex) {
exampleCodeRegExp = getRegexFromString(exampleCodeRegex);
}
if (rejectExampleCodeRegex) {
rejectExampleCodeRegExp = getRegexFromString(rejectExampleCodeRegex);
}
/**
* @type {{
* targetTagName: string,
* ext: string,
* codeStartLine: number,
* codeStartCol: number,
* nonJSPrefacingCols: number,
* commentLineCols: [number, number]
* }[]}
*/
const otherInfo = [];
/** @type {import('eslint').Linter.LintMessage[]} */
let extraMessages = [];
/**
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
* @param {string} jsFileName
* @param {[number, number]} commentLineCols
*/
const getTextsAndFileNames = (jsdoc, jsFileName, commentLineCols) => {
/**
* @type {{
* text: string,
* filename: string|null|undefined
* }[]}
*/
const textsAndFileNames = [];
/**
* @param {{
* filename: string|null,
* defaultFileName: string|undefined,
* source: string,
* targetTagName: string,
* rules?: import('eslint').Linter.RulesRecord|undefined,
* lines?: import('./iterateJsdoc.js').Integer,
* cols?: import('./iterateJsdoc.js').Integer,
* skipInit?: boolean,
* ext: string,
* sources?: {
* nonJSPrefacingCols: import('./iterateJsdoc.js').Integer,
* nonJSPrefacingLines: import('./iterateJsdoc.js').Integer,
* string: string,
* }[],
* tag?: import('comment-parser').Spec & {
* line?: import('./iterateJsdoc.js').Integer,
* }|{
* line: import('./iterateJsdoc.js').Integer,
* }
* }} cfg
*/
const checkSource = ({
filename,
ext,
defaultFileName,
lines = 0,
cols = 0,
skipInit,
source,
targetTagName,
sources = [],
tag = {
line: 0,
},
}) => {
if (!skipInit) {
sources.push({
nonJSPrefacingCols: cols,
nonJSPrefacingLines: lines,
string: source,
});
}
/**
* @param {{
* nonJSPrefacingCols: import('./iterateJsdoc.js').Integer,
* nonJSPrefacingLines: import('./iterateJsdoc.js').Integer,
* string: string
* }} cfg
*/
const addSourceInfo = function ({
nonJSPrefacingCols,
nonJSPrefacingLines,
string,
}) {
const src = paddedIndent ?
string.replaceAll(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gu'), '\n') :
string;
// Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api
const file = filename || defaultFileName;
if (!('line' in tag)) {
tag.line = tag.source[0].number;
}
// NOTE: `tag.line` can be 0 if of form `/** @tag ... */`
const codeStartLine = /**
* @type {import('comment-parser').Spec & {
* line: import('./iterateJsdoc.js').Integer,
* }}
*/ (tag).line + nonJSPrefacingLines;
const codeStartCol = likelyNestedJSDocIndentSpace;
textsAndFileNames.push({
text: src,
filename: file,
});
otherInfo.push({
targetTagName,
ext,
codeStartLine,
codeStartCol,
nonJSPrefacingCols,
commentLineCols
});
};
for (const targetSource of sources) {
addSourceInfo(targetSource);
}
};
/**
*
* @param {string|null} filename
* @param {string} [ext] Since `eslint-plugin-markdown` v2, and
* ESLint 7, this is the default which other JS-fenced rules will used.
* Formerly "md" was the default.
* @returns {{
* defaultFileName: string|undefined,
* filename: string|null,
* ext: string
* }}
*/
const getFilenameInfo = (filename, ext = 'md/*.js') => {
let defaultFileName;
if (!filename) {
if (typeof jsFileName === 'string' && jsFileName.includes('.')) {
defaultFileName = jsFileName.replace(/\.[^.]*$/u, `.${ext}`);
} else {
defaultFileName = `dummy.${ext}`;
}
}
return {
ext,
defaultFileName,
filename,
};
};
if (checkDefaults) {
const filenameInfo = getFilenameInfo(matchingFileNameDefaults, 'jsdoc-defaults');
forEachPreferredTag(jsdoc, 'default', (tag, targetTagName) => {
if (!tag.description.trim()) {
return;
}
checkSource({
source: `(${getTagDescription(tag)})`,
targetTagName,
...filenameInfo,
});
});
}
if (checkParams) {
const filenameInfo = getFilenameInfo(matchingFileNameParams, 'jsdoc-params');
forEachPreferredTag(jsdoc, 'param', (tag, targetTagName) => {
if (!tag.default || !tag.default.trim()) {
return;
}
checkSource({
source: `(${tag.default})`,
targetTagName,
...filenameInfo,
});
});
}
if (checkProperties) {
const filenameInfo = getFilenameInfo(matchingFileNameProperties, 'jsdoc-properties');
forEachPreferredTag(jsdoc, 'property', (tag, targetTagName) => {
if (!tag.default || !tag.default.trim()) {
return;
}
checkSource({
source: `(${tag.default})`,
targetTagName,
...filenameInfo,
});
});
}
if (!checkExamples) {
return textsAndFileNames;
}
const tagName = /** @type {string} */ (getPreferredTagName(jsdoc, {
tagName: 'example',
}));
if (!hasTag(jsdoc, tagName)) {
return textsAndFileNames;
}
const matchingFilenameInfo = getFilenameInfo(matchingFileName);
forEachPreferredTag(jsdoc, 'example', (tag, targetTagName) => {
let source = /** @type {string} */ (getTagDescription(tag));
const match = source.match(hasCaptionRegex);
if (captionRequired && (!match || !match[1].trim())) {
extraMessages.push({
line: 1 + commentLineCols[0] + (tag.line ?? tag.source[0].number),
column: commentLineCols[1] + 1,
severity: 2,
message: `@${targetTagName} error - Caption is expected for examples.`,
ruleId: 'jsdoc/example-missing-caption'
});
return;
}
source = source.replace(hasCaptionRegex, '');
const [
lines,
cols,
] = match ? getLinesCols(match[0]) : [
0, 0,
];
if (exampleCodeRegex && !exampleCodeRegExp.test(source) ||
rejectExampleCodeRegex && rejectExampleCodeRegExp.test(source)
) {
return;
}
const sources = [];
let skipInit = false;
if (exampleCodeRegex) {
let nonJSPrefacingCols = 0;
let nonJSPrefacingLines = 0;
let startingIndex = 0;
let lastStringCount = 0;
let exampleCode;
exampleCodeRegExp.lastIndex = 0;
while ((exampleCode = exampleCodeRegExp.exec(source)) !== null) {
const {
index,
'0': n0,
'1': n1,
} = exampleCode;
// Count anything preceding user regex match (can affect line numbering)
const preMatch = source.slice(startingIndex, index);
const [
preMatchLines,
colDelta,
] = getLinesCols(preMatch);
let nonJSPreface;
let nonJSPrefaceLineCount;
if (n1) {
const idx = n0.indexOf(n1);
nonJSPreface = n0.slice(0, idx);
nonJSPrefaceLineCount = countChars(nonJSPreface, '\n');
} else {
nonJSPreface = '';
nonJSPrefaceLineCount = 0;
}
nonJSPrefacingLines += lastStringCount + preMatchLines + nonJSPrefaceLineCount;
// Ignore `preMatch` delta if newlines here
if (nonJSPrefaceLineCount) {
const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length;
nonJSPrefacingCols += charsInLastLine;
} else {
nonJSPrefacingCols += colDelta + nonJSPreface.length;
}
const string = n1 || n0;
sources.push({
nonJSPrefacingCols,
nonJSPrefacingLines,
string,
});
startingIndex = exampleCodeRegExp.lastIndex;
lastStringCount = countChars(string, '\n');
if (!exampleCodeRegExp.global) {
break;
}
}
skipInit = true;
}
checkSource({
cols,
lines,
skipInit,
source,
sources,
tag,
targetTagName,
...matchingFilenameInfo,
});
});
return textsAndFileNames;
};
// See https://eslint.org/docs/latest/extend/plugins#processors-in-plugins
// See https://eslint.org/docs/latest/extend/custom-processors
// From https://github.com/eslint/eslint/issues/14745#issuecomment-869457265
/*
{
"files": ["*.js", "*.ts"],
"processor": "jsdoc/example" // a pretended value here
},
{
"files": [
"*.js/*_jsdoc-example.js",
"*.ts/*_jsdoc-example.js",
"*.js/*_jsdoc-example.ts"
],
"rules": {
// specific rules for examples in jsdoc only here
// And other rules for `.js` and `.ts` will also be enabled for them
}
}
*/
return {
meta: {
name: 'eslint-plugin-jsdoc/processor',
version,
},
processors: {
examples: {
meta: {
name: 'eslint-plugin-jsdoc/preprocessor',
version,
},
/**
* @param {string} text
* @param {string} filename
*/
preprocess (text, filename) {
try {
let ast;
// May be running a second time so catch and ignore
try {
ast = parser
// @ts-expect-error Ok
? parser.parseForESLint(text, {
ecmaVersion: 'latest',
sourceType,
comment: true
}).ast
: espree.parse(text, {
ecmaVersion: 'latest',
sourceType,
comment: true
});
} catch (err) {
return [text];
}
/** @type {[number, number][]} */
const commentLineCols = [];
const jsdocComments = /** @type {import('estree').Comment[]} */ (
/**
* @type {import('estree').Program & {
* comments?: import('estree').Comment[]
* }}
*/
(ast).comments
).filter((comment) => {
return (/^\*\s/u).test(comment.value);
}).map((comment) => {
/* c8 ignore next -- Unsupporting processors only? */
const [start] = comment.range ?? [];
const textToStart = text.slice(0, start);
const [lines, cols] = getLinesCols(textToStart);
// const lines = [...textToStart.matchAll(/\n/gu)].length
// const lastLinePos = textToStart.lastIndexOf('\n');
// const cols = lastLinePos === -1
// ? 0
// : textToStart.slice(lastLinePos).length;
commentLineCols.push([lines, cols]);
return parseComment(comment);
});
return [
text,
...jsdocComments.flatMap((jsdoc, idx) => {
return getTextsAndFileNames(
jsdoc,
filename,
commentLineCols[idx]
);
}).filter(Boolean)
];
/* c8 ignore next 3 */
} catch (err) {
console.log('err', filename, err);
}
},
/**
* @param {import('eslint').Linter.LintMessage[][]} messages
* @param {string} filename
*/
postprocess ([jsMessages, ...messages], filename) {
messages.forEach((message, idx) => {
const {
targetTagName,
codeStartLine,
codeStartCol,
nonJSPrefacingCols,
commentLineCols
} = otherInfo[idx];
message.forEach((msg) => {
const {
message,
ruleId,
severity,
fatal,
line,
column,
endColumn,
endLine,
// Todo: Make fixable
// fix
// fix: {range: [number, number], text: string}
// suggestions: {desc: , messageId:, fix: }[],
} = msg;
const [codeCtxLine, codeCtxColumn] = commentLineCols;
const startLine = codeCtxLine + codeStartLine + line;
const startCol = 1 + // Seems to need one more now
codeCtxColumn + codeStartCol + (
// This might not work for line 0, but line 0 is unlikely for examples
line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength
) + column;
msg.message = '@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') +
(ruleId ? ' (' + ruleId + ')' : '') + ': ' +
(fatal ? 'Fatal: ' : '') +
message;
msg.line = startLine;
msg.column = startCol;
msg.endLine = endLine ? startLine + endLine : startLine;
// added `- column` to offset what `endColumn` already seemed to include
msg.endColumn = endColumn ? startCol - column + endColumn : startCol;
});
});
const ret = [...jsMessages].concat(...messages, ...extraMessages);
extraMessages = [];
return ret;
},
supportsAutofix: true
},
},
};
};

11
node_modules/eslint-plugin-jsdoc/src/import-worker.mjs generated vendored Normal file
View File

@@ -0,0 +1,11 @@
import { runAsWorker } from 'synckit'
runAsWorker(async (imprt) => {
const { parseImports } = await import('parse-imports');
try {
// ESLint doesn't support async rules
return [...await parseImports(imprt)];
} catch (err) {
return false;
}
})

387
node_modules/eslint-plugin-jsdoc/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,387 @@
import checkAccess from './rules/checkAccess.js';
import checkAlignment from './rules/checkAlignment.js';
import checkExamples from './rules/checkExamples.js';
import checkIndentation from './rules/checkIndentation.js';
import checkLineAlignment from './rules/checkLineAlignment.js';
import checkParamNames from './rules/checkParamNames.js';
import checkPropertyNames from './rules/checkPropertyNames.js';
import checkSyntax from './rules/checkSyntax.js';
import checkTagNames from './rules/checkTagNames.js';
import checkTemplateNames from './rules/checkTemplateNames.js';
import checkTypes from './rules/checkTypes.js';
import checkValues from './rules/checkValues.js';
import convertToJsdocComments from './rules/convertToJsdocComments.js';
import emptyTags from './rules/emptyTags.js';
import implementsOnClasses from './rules/implementsOnClasses.js';
import importsAsDependencies from './rules/importsAsDependencies.js';
import informativeDocs from './rules/informativeDocs.js';
import matchDescription from './rules/matchDescription.js';
import matchName from './rules/matchName.js';
import multilineBlocks from './rules/multilineBlocks.js';
import noBadBlocks from './rules/noBadBlocks.js';
import noBlankBlockDescriptions from './rules/noBlankBlockDescriptions.js';
import noBlankBlocks from './rules/noBlankBlocks.js';
import noDefaults from './rules/noDefaults.js';
import noMissingSyntax from './rules/noMissingSyntax.js';
import noMultiAsterisks from './rules/noMultiAsterisks.js';
import noRestrictedSyntax from './rules/noRestrictedSyntax.js';
import noTypes from './rules/noTypes.js';
import noUndefinedTypes from './rules/noUndefinedTypes.js';
import requireAsteriskPrefix from './rules/requireAsteriskPrefix.js';
import requireDescription from './rules/requireDescription.js';
import requireDescriptionCompleteSentence from './rules/requireDescriptionCompleteSentence.js';
import requireExample from './rules/requireExample.js';
import requireFileOverview from './rules/requireFileOverview.js';
import requireHyphenBeforeParamDescription from './rules/requireHyphenBeforeParamDescription.js';
import requireJsdoc from './rules/requireJsdoc.js';
import requireParam from './rules/requireParam.js';
import requireParamDescription from './rules/requireParamDescription.js';
import requireParamName from './rules/requireParamName.js';
import requireParamType from './rules/requireParamType.js';
import requireProperty from './rules/requireProperty.js';
import requirePropertyDescription from './rules/requirePropertyDescription.js';
import requirePropertyName from './rules/requirePropertyName.js';
import requirePropertyType from './rules/requirePropertyType.js';
import requireReturns from './rules/requireReturns.js';
import requireReturnsCheck from './rules/requireReturnsCheck.js';
import requireReturnsDescription from './rules/requireReturnsDescription.js';
import requireReturnsType from './rules/requireReturnsType.js';
import requireTemplate from './rules/requireTemplate.js';
import requireThrows from './rules/requireThrows.js';
import requireYields from './rules/requireYields.js';
import requireYieldsCheck from './rules/requireYieldsCheck.js';
import sortTags from './rules/sortTags.js';
import tagLines from './rules/tagLines.js';
import textEscaping from './rules/textEscaping.js';
import validTypes from './rules/validTypes.js';
import { getJsdocProcessorPlugin } from './getJsdocProcessorPlugin.js';
/**
* @type {import('eslint').ESLint.Plugin & {
* configs: Record<
* "recommended"|"recommended-error"|"recommended-typescript"|
* "recommended-typescript-error"|"recommended-typescript-flavor"|
* "recommended-typescript-flavor-error"|"flat/recommended"|
* "flat/recommended-error"|"flat/recommended-typescript"|
* "flat/recommended-typescript-error"|
* "flat/recommended-typescript-flavor"|
* "flat/recommended-typescript-flavor-error",
* import('eslint').Linter.FlatConfig
* >
* }}
*/
const index = {
// @ts-expect-error Ok
configs: {},
rules: {
'check-access': checkAccess,
'check-alignment': checkAlignment,
'check-examples': checkExamples,
'check-indentation': checkIndentation,
'check-line-alignment': checkLineAlignment,
'check-param-names': checkParamNames,
'check-property-names': checkPropertyNames,
'check-syntax': checkSyntax,
'check-tag-names': checkTagNames,
'check-template-names': checkTemplateNames,
'check-types': checkTypes,
'check-values': checkValues,
'convert-to-jsdoc-comments': convertToJsdocComments,
'empty-tags': emptyTags,
'implements-on-classes': implementsOnClasses,
'imports-as-dependencies': importsAsDependencies,
'informative-docs': informativeDocs,
'match-description': matchDescription,
'match-name': matchName,
'multiline-blocks': multilineBlocks,
'no-bad-blocks': noBadBlocks,
'no-blank-block-descriptions': noBlankBlockDescriptions,
'no-blank-blocks': noBlankBlocks,
'no-defaults': noDefaults,
'no-missing-syntax': noMissingSyntax,
'no-multi-asterisks': noMultiAsterisks,
'no-restricted-syntax': noRestrictedSyntax,
'no-types': noTypes,
'no-undefined-types': noUndefinedTypes,
'require-asterisk-prefix': requireAsteriskPrefix,
'require-description': requireDescription,
'require-description-complete-sentence': requireDescriptionCompleteSentence,
'require-example': requireExample,
'require-file-overview': requireFileOverview,
'require-hyphen-before-param-description': requireHyphenBeforeParamDescription,
'require-jsdoc': requireJsdoc,
'require-param': requireParam,
'require-param-description': requireParamDescription,
'require-param-name': requireParamName,
'require-param-type': requireParamType,
'require-property': requireProperty,
'require-property-description': requirePropertyDescription,
'require-property-name': requirePropertyName,
'require-property-type': requirePropertyType,
'require-returns': requireReturns,
'require-returns-check': requireReturnsCheck,
'require-returns-description': requireReturnsDescription,
'require-returns-type': requireReturnsType,
'require-template': requireTemplate,
'require-throws': requireThrows,
'require-yields': requireYields,
'require-yields-check': requireYieldsCheck,
'sort-tags': sortTags,
'tag-lines': tagLines,
'text-escaping': textEscaping,
'valid-types': validTypes,
},
};
/**
* @param {"warn"|"error"} warnOrError
* @param {string} [flatName]
* @returns {import('eslint').Linter.FlatConfig}
*/
const createRecommendedRuleset = (warnOrError, flatName) => {
return {
...(flatName ? {name: 'jsdoc/' + flatName} : {}),
// @ts-expect-error Ok
plugins:
flatName ? {
jsdoc: index,
} : [
'jsdoc',
],
rules: {
'jsdoc/check-access': warnOrError,
'jsdoc/check-alignment': warnOrError,
'jsdoc/check-examples': 'off',
'jsdoc/check-indentation': 'off',
'jsdoc/check-line-alignment': 'off',
'jsdoc/check-param-names': warnOrError,
'jsdoc/check-property-names': warnOrError,
'jsdoc/check-syntax': 'off',
'jsdoc/check-tag-names': warnOrError,
'jsdoc/check-template-names': 'off',
'jsdoc/check-types': warnOrError,
'jsdoc/check-values': warnOrError,
'jsdoc/convert-to-jsdoc-comments': 'off',
'jsdoc/empty-tags': warnOrError,
'jsdoc/implements-on-classes': warnOrError,
'jsdoc/imports-as-dependencies': 'off',
'jsdoc/informative-docs': 'off',
'jsdoc/match-description': 'off',
'jsdoc/match-name': 'off',
'jsdoc/multiline-blocks': warnOrError,
'jsdoc/no-bad-blocks': 'off',
'jsdoc/no-blank-block-descriptions': 'off',
'jsdoc/no-blank-blocks': 'off',
'jsdoc/no-defaults': warnOrError,
'jsdoc/no-missing-syntax': 'off',
'jsdoc/no-multi-asterisks': warnOrError,
'jsdoc/no-restricted-syntax': 'off',
'jsdoc/no-types': 'off',
'jsdoc/no-undefined-types': warnOrError,
'jsdoc/require-asterisk-prefix': 'off',
'jsdoc/require-description': 'off',
'jsdoc/require-description-complete-sentence': 'off',
'jsdoc/require-example': 'off',
'jsdoc/require-file-overview': 'off',
'jsdoc/require-hyphen-before-param-description': 'off',
'jsdoc/require-jsdoc': warnOrError,
'jsdoc/require-param': warnOrError,
'jsdoc/require-param-description': warnOrError,
'jsdoc/require-param-name': warnOrError,
'jsdoc/require-param-type': warnOrError,
'jsdoc/require-property': warnOrError,
'jsdoc/require-property-description': warnOrError,
'jsdoc/require-property-name': warnOrError,
'jsdoc/require-property-type': warnOrError,
'jsdoc/require-returns': warnOrError,
'jsdoc/require-returns-check': warnOrError,
'jsdoc/require-returns-description': warnOrError,
'jsdoc/require-returns-type': warnOrError,
'jsdoc/require-template': 'off',
'jsdoc/require-throws': 'off',
'jsdoc/require-yields': warnOrError,
'jsdoc/require-yields-check': warnOrError,
'jsdoc/sort-tags': 'off',
'jsdoc/tag-lines': warnOrError,
'jsdoc/text-escaping': 'off',
'jsdoc/valid-types': warnOrError,
},
};
};
/**
* @param {"warn"|"error"} warnOrError
* @param {string} [flatName]
* @returns {import('eslint').Linter.FlatConfig}
*/
const createRecommendedTypeScriptRuleset = (warnOrError, flatName) => {
const ruleset = createRecommendedRuleset(warnOrError, flatName);
return {
...ruleset,
rules: {
...ruleset.rules,
/* eslint-disable indent -- Extra indent to avoid use by auto-rule-editing */
'jsdoc/check-tag-names': [
warnOrError, {
typed: true,
},
],
'jsdoc/no-types': warnOrError,
'jsdoc/no-undefined-types': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-property-type': 'off',
'jsdoc/require-returns-type': 'off',
/* eslint-enable indent */
},
};
};
/**
* @param {"warn"|"error"} warnOrError
* @param {string} [flatName]
* @returns {import('eslint').Linter.FlatConfig}
*/
const createRecommendedTypeScriptFlavorRuleset = (warnOrError, flatName) => {
const ruleset = createRecommendedRuleset(warnOrError, flatName);
return {
...ruleset,
rules: {
...ruleset.rules,
/* eslint-disable indent -- Extra indent to avoid use by auto-rule-editing */
'jsdoc/no-undefined-types': 'off',
/* eslint-enable indent */
},
};
};
/* c8 ignore next 3 -- TS */
if (!index.configs) {
throw new Error('TypeScript guard');
}
index.configs.recommended = createRecommendedRuleset('warn');
index.configs['recommended-error'] = createRecommendedRuleset('error');
index.configs['recommended-typescript'] = createRecommendedTypeScriptRuleset('warn');
index.configs['recommended-typescript-error'] = createRecommendedTypeScriptRuleset('error');
index.configs['recommended-typescript-flavor'] = createRecommendedTypeScriptFlavorRuleset('warn');
index.configs['recommended-typescript-flavor-error'] = createRecommendedTypeScriptFlavorRuleset('error');
index.configs['flat/recommended'] = createRecommendedRuleset('warn', 'flat/recommended');
index.configs['flat/recommended-error'] = createRecommendedRuleset('error', 'flat/recommended-error');
index.configs['flat/recommended-typescript'] = createRecommendedTypeScriptRuleset('warn', 'flat/recommended-typescript');
index.configs['flat/recommended-typescript-error'] = createRecommendedTypeScriptRuleset('error', 'flat/recommended-typescript-error');
index.configs['flat/recommended-typescript-flavor'] = createRecommendedTypeScriptFlavorRuleset('warn', 'flat/recommended-typescript-flavor');
index.configs['flat/recommended-typescript-flavor-error'] = createRecommendedTypeScriptFlavorRuleset('error', 'flat/recommended-typescript-flavor-error');
index.configs.examples = /** @type {import('eslint').Linter.FlatConfig[]} */ ([
{
name: 'jsdoc/examples/processor',
files: ['**/*.js'],
plugins: {
examples: getJsdocProcessorPlugin()
},
processor: 'examples/examples',
},
{
name: 'jsdoc/examples/rules',
files: ['**/*.md/*.js'],
rules: {
// "always" newline rule at end unlikely in sample code
'eol-last': 0,
// Wouldn't generally expect example paths to resolve relative to JS file
'import/no-unresolved': 0,
// Snippets likely too short to always include import/export info
'import/unambiguous': 0,
'jsdoc/require-file-overview': 0,
// The end of a multiline comment would end the comment the example is in.
'jsdoc/require-jsdoc': 0,
// Unlikely to have inadvertent debugging within examples
'no-console': 0,
// Often wish to start `@example` code after newline; also may use
// empty lines for spacing
'no-multiple-empty-lines': 0,
// Many variables in examples will be `undefined`
'no-undef': 0,
// Common to define variables for clarity without always using them
'no-unused-vars': 0,
// See import/no-unresolved
'node/no-missing-import': 0,
'node/no-missing-require': 0,
// Can generally look nicer to pad a little even if code imposes more stringency
'padded-blocks': 0,
}
}
]);
index.configs['default-expressions'] = /** @type {import('eslint').Linter.FlatConfig[]} */ ([
{
files: ['**/*.js'],
name: 'jsdoc/default-expressions/processor',
plugins: {
examples: getJsdocProcessorPlugin({
checkDefaults: true,
checkParams: true,
checkProperties: true
})
},
processor: 'examples/examples'
},
{
name: 'jsdoc/default-expressions/rules',
files: ['**/*.jsdoc-defaults', '**/*.jsdoc-params', '**/*.jsdoc-properties'],
rules: {
...index.configs.examples[1].rules,
'chai-friendly/no-unused-expressions': 0,
'no-empty-function': 0,
'no-new': 0,
'no-unused-expressions': 0,
quotes: [
'error', 'double',
],
semi: [
'error', 'never',
],
strict: 0
},
}
]);
index.configs['examples-and-default-expressions'] = /** @type {import('eslint').Linter.FlatConfig[]} */ ([
{
name: 'jsdoc/examples-and-default-expressions',
plugins: {
examples: getJsdocProcessorPlugin({
checkDefaults: true,
checkParams: true,
checkProperties: true
})
},
},
...index.configs.examples.map((config) => {
return {
...config,
plugins: {}
};
}),
...index.configs['default-expressions'].map((config) => {
return {
...config,
plugins: {}
};
})
]);
export default index;

2479
node_modules/eslint-plugin-jsdoc/src/iterateJsdoc.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1880
node_modules/eslint-plugin-jsdoc/src/jsdocUtils.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
import iterateJsdoc from '../iterateJsdoc.js';
const accessLevels = [
'package', 'private', 'protected', 'public',
];
export default iterateJsdoc(({
report,
utils,
}) => {
utils.forEachPreferredTag('access', (jsdocParameter, targetTagName) => {
const desc = jsdocParameter.name + ' ' + jsdocParameter.description;
if (!accessLevels.includes(desc.trim())) {
report(
`Missing valid JSDoc @${targetTagName} level.`,
null,
jsdocParameter,
);
}
});
const accessLength = utils.getTags('access').length;
const individualTagLength = utils.getPresentTags(accessLevels).length;
if (accessLength && individualTagLength) {
report(
'The @access tag may not be used with specific access-control tags (@package, @private, @protected, or @public).',
);
}
if (accessLength > 1 || individualTagLength > 1) {
report(
'At most one access-control tag may be present on a jsdoc block.',
);
}
}, {
checkPrivate: true,
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Checks that `@access` tags have a valid value.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-access.md#repos-sticky-header',
},
type: 'suggestion',
},
});

View File

@@ -0,0 +1,63 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {string} string
* @returns {string}
*/
const trimStart = (string) => {
return string.replace(/^\s+/u, '');
};
export default iterateJsdoc(({
sourceCode,
jsdocNode,
report,
indent,
}) => {
// `indent` is whitespace from line 1 (`/**`), so slice and account for "/".
const indentLevel = indent.length + 1;
const sourceLines = sourceCode.getText(jsdocNode).split('\n')
.slice(1)
.map((line) => {
return line.split('*')[0];
})
.filter((line) => {
return !trimStart(line).length;
});
/** @type {import('eslint').Rule.ReportFixer} */
const fix = (fixer) => {
const replacement = sourceCode.getText(jsdocNode).split('\n')
.map((line, index) => {
// Ignore the first line and all lines not starting with `*`
const ignored = !index || trimStart(line.split('*')[0]).length;
return ignored ? line : `${indent} ${trimStart(line)}`;
})
.join('\n');
return fixer.replaceText(jsdocNode, replacement);
};
sourceLines.some((line, lineNum) => {
if (line.length !== indentLevel) {
report('Expected JSDoc block to be aligned.', fix, {
line: lineNum + 1,
});
return true;
}
return false;
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports invalid alignment of JSDoc block asterisks.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-alignment.md#repos-sticky-header',
},
fixable: 'code',
type: 'layout',
},
});

View File

@@ -0,0 +1,593 @@
// Todo: When replace `CLIEngine` with `ESLint` when feature set complete per https://github.com/eslint/eslint/issues/14745
// https://github.com/eslint/eslint/blob/master/docs/user-guide/migrating-to-7.0.0.md#-the-cliengine-class-has-been-deprecated
import iterateJsdoc from '../iterateJsdoc.js';
import eslint, {
ESLint,
} from 'eslint';
import semver from 'semver';
const {
// @ts-expect-error Older ESLint
CLIEngine,
} = eslint;
const zeroBasedLineIndexAdjust = -1;
const likelyNestedJSDocIndentSpace = 1;
const preTagSpaceLength = 1;
// If a space is present, we should ignore it
const firstLinePrefixLength = preTagSpaceLength;
const hasCaptionRegex = /^\s*<caption>([\s\S]*?)<\/caption>/u;
/**
* @param {string} str
* @returns {string}
*/
const escapeStringRegexp = (str) => {
return str.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
};
/**
* @param {string} str
* @param {string} ch
* @returns {import('../iterateJsdoc.js').Integer}
*/
const countChars = (str, ch) => {
return (str.match(new RegExp(escapeStringRegexp(ch), 'gu')) || []).length;
};
/** @type {import('eslint').Linter.RulesRecord} */
const defaultMdRules = {
// "always" newline rule at end unlikely in sample code
'eol-last': 0,
// Wouldn't generally expect example paths to resolve relative to JS file
'import/no-unresolved': 0,
// Snippets likely too short to always include import/export info
'import/unambiguous': 0,
'jsdoc/require-file-overview': 0,
// The end of a multiline comment would end the comment the example is in.
'jsdoc/require-jsdoc': 0,
// Unlikely to have inadvertent debugging within examples
'no-console': 0,
// Often wish to start `@example` code after newline; also may use
// empty lines for spacing
'no-multiple-empty-lines': 0,
// Many variables in examples will be `undefined`
'no-undef': 0,
// Common to define variables for clarity without always using them
'no-unused-vars': 0,
// See import/no-unresolved
'node/no-missing-import': 0,
'node/no-missing-require': 0,
// Can generally look nicer to pad a little even if code imposes more stringency
'padded-blocks': 0,
};
/** @type {import('eslint').Linter.RulesRecord} */
const defaultExpressionRules = {
...defaultMdRules,
'chai-friendly/no-unused-expressions': 'off',
'no-empty-function': 'off',
'no-new': 'off',
'no-unused-expressions': 'off',
quotes: [
'error', 'double',
],
semi: [
'error', 'never',
],
strict: 'off',
};
/**
* @param {string} text
* @returns {[
* import('../iterateJsdoc.js').Integer,
* import('../iterateJsdoc.js').Integer
* ]}
*/
const getLinesCols = (text) => {
const matchLines = countChars(text, '\n');
const colDelta = matchLines ?
text.slice(text.lastIndexOf('\n') + 1).length :
text.length;
return [
matchLines, colDelta,
];
};
export default iterateJsdoc(({
report,
utils,
context,
globalState,
}) => {
if (semver.gte(ESLint.version, '8.0.0')) {
report(
'This rule does not work for ESLint 8+; you should disable this rule and use' +
'the processor mentioned in the docs.',
null,
{
column: 1,
line: 1,
},
);
return;
}
if (!globalState.has('checkExamples-matchingFileName')) {
globalState.set('checkExamples-matchingFileName', new Map());
}
const matchingFileNameMap = /** @type {Map<string, string>} */ (
globalState.get('checkExamples-matchingFileName')
);
const options = context.options[0] || {};
let {
exampleCodeRegex = null,
rejectExampleCodeRegex = null,
} = options;
const {
checkDefaults = false,
checkParams = false,
checkProperties = false,
noDefaultExampleRules = false,
checkEslintrc = true,
matchingFileName = null,
matchingFileNameDefaults = null,
matchingFileNameParams = null,
matchingFileNameProperties = null,
paddedIndent = 0,
baseConfig = {},
configFile,
allowInlineConfig = true,
reportUnusedDisableDirectives = true,
captionRequired = false,
} = options;
// Make this configurable?
/**
* @type {never[]}
*/
const rulePaths = [];
const mdRules = noDefaultExampleRules ? undefined : defaultMdRules;
const expressionRules = noDefaultExampleRules ? undefined : defaultExpressionRules;
if (exampleCodeRegex) {
exampleCodeRegex = utils.getRegexFromString(exampleCodeRegex);
}
if (rejectExampleCodeRegex) {
rejectExampleCodeRegex = utils.getRegexFromString(rejectExampleCodeRegex);
}
/**
* @param {{
* filename: string,
* defaultFileName: string|undefined,
* source: string,
* targetTagName: string,
* rules?: import('eslint').Linter.RulesRecord|undefined,
* lines?: import('../iterateJsdoc.js').Integer,
* cols?: import('../iterateJsdoc.js').Integer,
* skipInit?: boolean,
* sources?: {
* nonJSPrefacingCols: import('../iterateJsdoc.js').Integer,
* nonJSPrefacingLines: import('../iterateJsdoc.js').Integer,
* string: string,
* }[],
* tag?: import('comment-parser').Spec & {
* line?: import('../iterateJsdoc.js').Integer,
* }|{
* line: import('../iterateJsdoc.js').Integer,
* }
* }} cfg
*/
const checkSource = ({
filename,
defaultFileName,
rules = expressionRules,
lines = 0,
cols = 0,
skipInit,
source,
targetTagName,
sources = [],
tag = {
line: 0,
},
}) => {
if (!skipInit) {
sources.push({
nonJSPrefacingCols: cols,
nonJSPrefacingLines: lines,
string: source,
});
}
// Todo: Make fixable
/**
* @param {{
* nonJSPrefacingCols: import('../iterateJsdoc.js').Integer,
* nonJSPrefacingLines: import('../iterateJsdoc.js').Integer,
* string: string
* }} cfg
*/
const checkRules = function ({
nonJSPrefacingCols,
nonJSPrefacingLines,
string,
}) {
const cliConfig = {
allowInlineConfig,
baseConfig,
configFile,
reportUnusedDisableDirectives,
rulePaths,
rules,
useEslintrc: checkEslintrc,
};
const cliConfigStr = JSON.stringify(cliConfig);
const src = paddedIndent ?
string.replaceAll(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gu'), '\n') :
string;
// Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api
const fileNameMapKey = filename ?
'a' + cliConfigStr + filename :
'b' + cliConfigStr + defaultFileName;
const file = filename || defaultFileName;
let cliFile;
if (matchingFileNameMap.has(fileNameMapKey)) {
cliFile = matchingFileNameMap.get(fileNameMapKey);
} else {
const cli = new CLIEngine(cliConfig);
let config;
if (filename || checkEslintrc) {
config = cli.getConfigForFile(file);
}
// We need a new instance to ensure that the rules that may only
// be available to `file` (if it has its own `.eslintrc`),
// will be defined.
cliFile = new CLIEngine({
allowInlineConfig,
baseConfig: {
...baseConfig,
...config,
},
configFile,
reportUnusedDisableDirectives,
rulePaths,
rules,
useEslintrc: false,
});
matchingFileNameMap.set(fileNameMapKey, cliFile);
}
const {
results: [
{
messages,
},
],
} = cliFile.executeOnText(src);
if (!('line' in tag)) {
tag.line = tag.source[0].number;
}
// NOTE: `tag.line` can be 0 if of form `/** @tag ... */`
const codeStartLine = /**
* @type {import('comment-parser').Spec & {
* line: import('../iterateJsdoc.js').Integer,
* }}
*/ (tag).line + nonJSPrefacingLines;
const codeStartCol = likelyNestedJSDocIndentSpace;
for (const {
message,
line,
column,
severity,
ruleId,
} of messages) {
const startLine = codeStartLine + line + zeroBasedLineIndexAdjust;
const startCol = codeStartCol + (
// This might not work for line 0, but line 0 is unlikely for examples
line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength
) + column;
report(
'@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') +
(ruleId ? ' (' + ruleId + ')' : '') + ': ' +
message,
null,
{
column: startCol,
line: startLine,
},
);
}
};
for (const targetSource of sources) {
checkRules(targetSource);
}
};
/**
*
* @param {string} filename
* @param {string} [ext] Since `eslint-plugin-markdown` v2, and
* ESLint 7, this is the default which other JS-fenced rules will used.
* Formerly "md" was the default.
* @returns {{defaultFileName: string|undefined, filename: string}}
*/
const getFilenameInfo = (filename, ext = 'md/*.js') => {
let defaultFileName;
if (!filename) {
const jsFileName = context.getFilename();
if (typeof jsFileName === 'string' && jsFileName.includes('.')) {
defaultFileName = jsFileName.replace(/\.[^.]*$/u, `.${ext}`);
} else {
defaultFileName = `dummy.${ext}`;
}
}
return {
defaultFileName,
filename,
};
};
if (checkDefaults) {
const filenameInfo = getFilenameInfo(matchingFileNameDefaults, 'jsdoc-defaults');
utils.forEachPreferredTag('default', (tag, targetTagName) => {
if (!tag.description.trim()) {
return;
}
checkSource({
source: `(${utils.getTagDescription(tag)})`,
targetTagName,
...filenameInfo,
});
});
}
if (checkParams) {
const filenameInfo = getFilenameInfo(matchingFileNameParams, 'jsdoc-params');
utils.forEachPreferredTag('param', (tag, targetTagName) => {
if (!tag.default || !tag.default.trim()) {
return;
}
checkSource({
source: `(${tag.default})`,
targetTagName,
...filenameInfo,
});
});
}
if (checkProperties) {
const filenameInfo = getFilenameInfo(matchingFileNameProperties, 'jsdoc-properties');
utils.forEachPreferredTag('property', (tag, targetTagName) => {
if (!tag.default || !tag.default.trim()) {
return;
}
checkSource({
source: `(${tag.default})`,
targetTagName,
...filenameInfo,
});
});
}
const tagName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'example',
}));
if (!utils.hasTag(tagName)) {
return;
}
const matchingFilenameInfo = getFilenameInfo(matchingFileName);
utils.forEachPreferredTag('example', (tag, targetTagName) => {
let source = /** @type {string} */ (utils.getTagDescription(tag));
const match = source.match(hasCaptionRegex);
if (captionRequired && (!match || !match[1].trim())) {
report('Caption is expected for examples.', null, tag);
}
source = source.replace(hasCaptionRegex, '');
const [
lines,
cols,
] = match ? getLinesCols(match[0]) : [
0, 0,
];
if (exampleCodeRegex && !exampleCodeRegex.test(source) ||
rejectExampleCodeRegex && rejectExampleCodeRegex.test(source)
) {
return;
}
const sources = [];
let skipInit = false;
if (exampleCodeRegex) {
let nonJSPrefacingCols = 0;
let nonJSPrefacingLines = 0;
let startingIndex = 0;
let lastStringCount = 0;
let exampleCode;
exampleCodeRegex.lastIndex = 0;
while ((exampleCode = exampleCodeRegex.exec(source)) !== null) {
const {
index,
'0': n0,
'1': n1,
} = exampleCode;
// Count anything preceding user regex match (can affect line numbering)
const preMatch = source.slice(startingIndex, index);
const [
preMatchLines,
colDelta,
] = getLinesCols(preMatch);
let nonJSPreface;
let nonJSPrefaceLineCount;
if (n1) {
const idx = n0.indexOf(n1);
nonJSPreface = n0.slice(0, idx);
nonJSPrefaceLineCount = countChars(nonJSPreface, '\n');
} else {
nonJSPreface = '';
nonJSPrefaceLineCount = 0;
}
nonJSPrefacingLines += lastStringCount + preMatchLines + nonJSPrefaceLineCount;
// Ignore `preMatch` delta if newlines here
if (nonJSPrefaceLineCount) {
const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length;
nonJSPrefacingCols += charsInLastLine;
} else {
nonJSPrefacingCols += colDelta + nonJSPreface.length;
}
const string = n1 || n0;
sources.push({
nonJSPrefacingCols,
nonJSPrefacingLines,
string,
});
startingIndex = exampleCodeRegex.lastIndex;
lastStringCount = countChars(string, '\n');
if (!exampleCodeRegex.global) {
break;
}
}
skipInit = true;
}
checkSource({
cols,
lines,
rules: mdRules,
skipInit,
source,
sources,
tag,
targetTagName,
...matchingFilenameInfo,
});
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Ensures that (JavaScript) examples within JSDoc adhere to ESLint rules.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-examples.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
allowInlineConfig: {
default: true,
type: 'boolean',
},
baseConfig: {
type: 'object',
},
captionRequired: {
default: false,
type: 'boolean',
},
checkDefaults: {
default: false,
type: 'boolean',
},
checkEslintrc: {
default: true,
type: 'boolean',
},
checkParams: {
default: false,
type: 'boolean',
},
checkProperties: {
default: false,
type: 'boolean',
},
configFile: {
type: 'string',
},
exampleCodeRegex: {
type: 'string',
},
matchingFileName: {
type: 'string',
},
matchingFileNameDefaults: {
type: 'string',
},
matchingFileNameParams: {
type: 'string',
},
matchingFileNameProperties: {
type: 'string',
},
noDefaultExampleRules: {
default: false,
type: 'boolean',
},
paddedIndent: {
default: 0,
type: 'integer',
},
rejectExampleCodeRegex: {
type: 'string',
},
reportUnusedDisableDirectives: {
default: true,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,75 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {string} str
* @param {string[]} excludeTags
* @returns {string}
*/
const maskExcludedContent = (str, excludeTags) => {
const regContent = new RegExp(`([ \\t]+\\*)[ \\t]@(?:${excludeTags.join('|')})(?=[ \\n])([\\w|\\W]*?\\n)(?=[ \\t]*\\*(?:[ \\t]*@\\w+\\s|\\/))`, 'gu');
return str.replace(regContent, (_match, margin, code) => {
return (margin + '\n').repeat(code.match(/\n/gu).length);
});
};
/**
* @param {string} str
* @returns {string}
*/
const maskCodeBlocks = (str) => {
const regContent = /([ \t]+\*)[ \t]```[^\n]*?([\w|\W]*?\n)(?=[ \t]*\*(?:[ \t]*(?:```|@\w+\s)|\/))/gu;
return str.replaceAll(regContent, (_match, margin, code) => {
return (margin + '\n').repeat(code.match(/\n/gu).length);
});
};
export default iterateJsdoc(({
sourceCode,
jsdocNode,
report,
context,
}) => {
const options = context.options[0] || {};
const /** @type {{excludeTags: string[]}} */ {
excludeTags = [
'example',
],
} = options;
const reg = /^(?:\/?\**|[ \t]*)\*[ \t]{2}/gmu;
const textWithoutCodeBlocks = maskCodeBlocks(sourceCode.getText(jsdocNode));
const text = excludeTags.length ? maskExcludedContent(textWithoutCodeBlocks, excludeTags) : textWithoutCodeBlocks;
if (reg.test(text)) {
const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/gu) || [];
report('There must be no indentation.', null, {
line: lineBreaks.length,
});
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports invalid padding inside JSDoc blocks.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-indentation.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
excludeTags: {
items: {
pattern: '^\\S+$',
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'layout',
},
});

View File

@@ -0,0 +1,372 @@
import alignTransform from '../alignTransform.js';
import iterateJsdoc from '../iterateJsdoc.js';
import {
transforms,
} from 'comment-parser';
const {
flow: commentFlow,
} = transforms;
/**
* @typedef {{
* postDelimiter: import('../iterateJsdoc.js').Integer,
* postHyphen: import('../iterateJsdoc.js').Integer,
* postName: import('../iterateJsdoc.js').Integer,
* postTag: import('../iterateJsdoc.js').Integer,
* postType: import('../iterateJsdoc.js').Integer,
* }} CustomSpacings
*/
/**
* @param {import('../iterateJsdoc.js').Utils} utils
* @param {import('comment-parser').Spec & {
* line: import('../iterateJsdoc.js').Integer
* }} tag
* @param {CustomSpacings} customSpacings
*/
const checkNotAlignedPerTag = (utils, tag, customSpacings) => {
/*
start +
delimiter +
postDelimiter +
tag +
postTag +
type +
postType +
name +
postName +
description +
end +
lineEnd
*/
/**
* @typedef {"tag"|"type"|"name"|"description"} ContentProp
*/
/** @type {("postDelimiter"|"postTag"|"postType"|"postName")[]} */
let spacerProps;
/** @type {ContentProp[]} */
let contentProps;
const mightHaveNamepath = utils.tagMightHaveNamepath(tag.tag);
if (mightHaveNamepath) {
spacerProps = [
'postDelimiter', 'postTag', 'postType', 'postName',
];
contentProps = [
'tag', 'type', 'name', 'description',
];
} else {
spacerProps = [
'postDelimiter', 'postTag', 'postType',
];
contentProps = [
'tag', 'type', 'description',
];
}
const {
tokens,
} = tag.source[0];
/**
* @param {import('../iterateJsdoc.js').Integer} idx
* @param {(notRet: boolean, contentProp: ContentProp) => void} [callbck]
*/
const followedBySpace = (idx, callbck) => {
const nextIndex = idx + 1;
return spacerProps.slice(nextIndex).some((spacerProp, innerIdx) => {
const contentProp = contentProps[nextIndex + innerIdx];
const spacePropVal = tokens[spacerProp];
const ret = spacePropVal;
if (callbck) {
callbck(!ret, contentProp);
}
return ret && (callbck || !contentProp);
});
};
const postHyphenSpacing = customSpacings?.postHyphen ?? 1;
const exactHyphenSpacing = new RegExp(`^\\s*-\\s{${postHyphenSpacing},${postHyphenSpacing}}(?!\\s)`, 'u');
const hasNoHyphen = !(/^\s*-(?!$)(?=\s)/u).test(tokens.description);
const hasExactHyphenSpacing = exactHyphenSpacing.test(
tokens.description,
);
// If checking alignment on multiple lines, need to check other `source`
// items
// Go through `post*` spacing properties and exit to indicate problem if
// extra spacing detected
const ok = !spacerProps.some((spacerProp, idx) => {
const contentProp = contentProps[idx];
const contentPropVal = tokens[contentProp];
const spacerPropVal = tokens[spacerProp];
const spacing = customSpacings?.[spacerProp] || 1;
// There will be extra alignment if...
// 1. The spaces don't match the space it should have (1 or custom spacing) OR
return spacerPropVal.length !== spacing && spacerPropVal.length !== 0 ||
// 2. There is a (single) space, no immediate content, and yet another
// space is found subsequently (not separated by intervening content)
spacerPropVal && !contentPropVal && followedBySpace(idx);
}) && (hasNoHyphen || hasExactHyphenSpacing);
if (ok) {
return;
}
const fix = () => {
for (const [
idx,
spacerProp,
] of spacerProps.entries()) {
const contentProp = contentProps[idx];
const contentPropVal = tokens[contentProp];
if (contentPropVal) {
const spacing = customSpacings?.[spacerProp] || 1;
tokens[spacerProp] = ''.padStart(spacing, ' ');
followedBySpace(idx, (hasSpace, contentPrp) => {
if (hasSpace) {
tokens[contentPrp] = '';
}
});
} else {
tokens[spacerProp] = '';
}
}
if (!hasExactHyphenSpacing) {
const hyphenSpacing = /^\s*-\s+/u;
tokens.description = tokens.description.replace(
hyphenSpacing, '-' + ''.padStart(postHyphenSpacing, ' '),
);
}
utils.setTag(tag, tokens);
};
utils.reportJSDoc('Expected JSDoc block lines to not be aligned.', tag, fix, true);
};
/**
* @param {object} cfg
* @param {CustomSpacings} cfg.customSpacings
* @param {string} cfg.indent
* @param {import('comment-parser').Block} cfg.jsdoc
* @param {import('eslint').Rule.Node & {
* range: [number, number]
* }} cfg.jsdocNode
* @param {boolean} cfg.preserveMainDescriptionPostDelimiter
* @param {import('../iterateJsdoc.js').Report} cfg.report
* @param {string[]} cfg.tags
* @param {import('../iterateJsdoc.js').Utils} cfg.utils
* @param {string} cfg.wrapIndent
* @param {boolean} cfg.disableWrapIndent
* @returns {void}
*/
const checkAlignment = ({
customSpacings,
indent,
jsdoc,
jsdocNode,
preserveMainDescriptionPostDelimiter,
report,
tags,
utils,
wrapIndent,
disableWrapIndent,
}) => {
const transform = commentFlow(
alignTransform({
customSpacings,
indent,
preserveMainDescriptionPostDelimiter,
tags,
wrapIndent,
disableWrapIndent,
}),
);
const transformedJsdoc = transform(jsdoc);
const comment = '/*' +
/**
* @type {import('eslint').Rule.Node & {
* range: [number, number], value: string
* }}
*/ (jsdocNode).value + '*/';
const formatted = utils.stringify(transformedJsdoc)
.trimStart();
if (comment !== formatted) {
report(
'Expected JSDoc block lines to be aligned.',
/** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
return fixer.replaceText(jsdocNode, formatted);
},
);
}
};
export default iterateJsdoc(({
indent,
jsdoc,
jsdocNode,
report,
context,
utils,
}) => {
const {
tags: applicableTags = [
'param', 'arg', 'argument', 'property', 'prop', 'returns', 'return',
],
preserveMainDescriptionPostDelimiter,
customSpacings,
wrapIndent = '',
disableWrapIndent = false,
} = context.options[1] || {};
if (context.options[0] === 'always') {
// Skip if it contains only a single line.
if (!(
/**
* @type {import('eslint').Rule.Node & {
* range: [number, number], value: string
* }}
*/
(jsdocNode).value.includes('\n')
)) {
return;
}
checkAlignment({
customSpacings,
indent,
jsdoc,
jsdocNode,
preserveMainDescriptionPostDelimiter,
report,
tags: applicableTags,
utils,
wrapIndent,
disableWrapIndent,
});
return;
}
const foundTags = utils.getPresentTags(applicableTags);
if (context.options[0] !== 'any') {
for (const tag of foundTags) {
checkNotAlignedPerTag(
utils,
/**
* @type {import('comment-parser').Spec & {
* line: import('../iterateJsdoc.js').Integer
* }}
*/
(tag),
customSpacings,
);
}
}
for (const tag of foundTags) {
if (tag.source.length > 1) {
let idx = 0;
for (const {
tokens,
// Avoid the tag line
} of tag.source.slice(1)) {
idx++;
if (
!tokens.description ||
// Avoid first lines after multiline type
tokens.type ||
tokens.name
) {
continue;
}
// Don't include a single separating space/tab
if (!disableWrapIndent && tokens.postDelimiter.slice(1) !== wrapIndent) {
utils.reportJSDoc('Expected wrap indent', {
line: tag.source[0].number + idx,
}, () => {
tokens.postDelimiter = tokens.postDelimiter.charAt(0) + wrapIndent;
});
return;
}
}
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports invalid alignment of JSDoc block lines.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-line-alignment.md#repos-sticky-header',
},
fixable: 'whitespace',
schema: [
{
enum: [
'always', 'never', 'any',
],
type: 'string',
},
{
additionalProperties: false,
properties: {
customSpacings: {
additionalProperties: false,
properties: {
postDelimiter: {
type: 'integer',
},
postHyphen: {
type: 'integer',
},
postName: {
type: 'integer',
},
postTag: {
type: 'integer',
},
postType: {
type: 'integer',
},
},
},
preserveMainDescriptionPostDelimiter: {
default: false,
type: 'boolean',
},
tags: {
items: {
type: 'string',
},
type: 'array',
},
wrapIndent: {
type: 'string',
},
disableWrapIndent: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'layout',
},
});

View File

@@ -0,0 +1,454 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {string} targetTagName
* @param {boolean} allowExtraTrailingParamDocs
* @param {boolean} checkDestructured
* @param {boolean} checkRestProperty
* @param {RegExp} checkTypesRegex
* @param {boolean} disableExtraPropertyReporting
* @param {boolean} disableMissingParamChecks
* @param {boolean} enableFixer
* @param {import('../jsdocUtils.js').ParamNameInfo[]} functionParameterNames
* @param {import('comment-parser').Block} jsdoc
* @param {import('../iterateJsdoc.js').Utils} utils
* @param {import('../iterateJsdoc.js').Report} report
* @returns {boolean}
*/
const validateParameterNames = (
targetTagName,
allowExtraTrailingParamDocs,
checkDestructured,
checkRestProperty,
checkTypesRegex,
disableExtraPropertyReporting,
disableMissingParamChecks,
enableFixer,
functionParameterNames, jsdoc, utils, report,
) => {
const paramTags = Object.entries(jsdoc.tags).filter(([
, tag,
]) => {
return tag.tag === targetTagName;
});
const paramTagsNonNested = paramTags.filter(([
, tag,
]) => {
return !tag.name.includes('.');
});
let dotted = 0;
let thisOffset = 0;
// eslint-disable-next-line complexity
return paramTags.some(([
, tag,
], index) => {
/** @type {import('../iterateJsdoc.js').Integer} */
let tagsIndex;
const dupeTagInfo = paramTags.find(([
tgsIndex,
tg,
], idx) => {
tagsIndex = Number(tgsIndex);
return tg.name === tag.name && idx !== index;
});
if (dupeTagInfo) {
utils.reportJSDoc(`Duplicate @${targetTagName} "${tag.name}"`, dupeTagInfo[1], enableFixer ? () => {
utils.removeTag(tagsIndex);
} : null);
return true;
}
if (tag.name.includes('.')) {
dotted++;
return false;
}
let functionParameterName = functionParameterNames[index - dotted + thisOffset];
if (functionParameterName === 'this' && tag.name.trim() !== 'this') {
++thisOffset;
functionParameterName = functionParameterNames[index - dotted + thisOffset];
}
if (!functionParameterName) {
if (allowExtraTrailingParamDocs) {
return false;
}
report(
`@${targetTagName} "${tag.name}" does not match an existing function parameter.`,
null,
tag,
);
return true;
}
if (
typeof functionParameterName === 'object' &&
'name' in functionParameterName &&
Array.isArray(functionParameterName.name)
) {
const actualName = tag.name.trim();
const expectedName = functionParameterName.name[index];
if (actualName === expectedName) {
thisOffset--;
return false;
}
report(
`Expected @${targetTagName} name to be "${expectedName}". Got "${actualName}".`,
null,
tag,
);
return true;
}
if (Array.isArray(functionParameterName)) {
if (!checkDestructured) {
return false;
}
if (tag.type && tag.type.search(checkTypesRegex) === -1) {
return false;
}
const [
parameterName,
{
names: properties,
hasPropertyRest,
rests,
annotationParamName,
},
] =
/**
* @type {[string | undefined, import('../jsdocUtils.js').FlattendRootInfo & {
* annotationParamName?: string | undefined;
}]} */ (functionParameterName);
if (annotationParamName !== undefined) {
const name = tag.name.trim();
if (name !== annotationParamName) {
report(`@${targetTagName} "${name}" does not match parameter name "${annotationParamName}"`, null, tag);
}
}
const tagName = parameterName === undefined ? tag.name.trim() : parameterName;
const expectedNames = properties.map((name) => {
return `${tagName}.${name}`;
});
const actualNames = paramTags.map(([
, paramTag,
]) => {
return paramTag.name.trim();
});
const actualTypes = paramTags.map(([
, paramTag,
]) => {
return paramTag.type;
});
const missingProperties = [];
/** @type {string[]} */
const notCheckingNames = [];
for (const [
idx,
name,
] of expectedNames.entries()) {
if (notCheckingNames.some((notCheckingName) => {
return name.startsWith(notCheckingName);
})) {
continue;
}
const actualNameIdx = actualNames.findIndex((actualName) => {
return utils.comparePaths(name)(actualName);
});
if (actualNameIdx === -1) {
if (!checkRestProperty && rests[idx]) {
continue;
}
const missingIndex = actualNames.findIndex((actualName) => {
return utils.pathDoesNotBeginWith(name, actualName);
});
const line = tag.source[0].number - 1 + (missingIndex > -1 ? missingIndex : actualNames.length);
missingProperties.push({
name,
tagPlacement: {
line: line === 0 ? 1 : line,
},
});
} else if (actualTypes[actualNameIdx].search(checkTypesRegex) === -1 && actualTypes[actualNameIdx] !== '') {
notCheckingNames.push(name);
}
}
const hasMissing = missingProperties.length;
if (hasMissing) {
for (const {
tagPlacement,
name: missingProperty,
} of missingProperties) {
report(`Missing @${targetTagName} "${missingProperty}"`, null, tagPlacement);
}
}
if (!hasPropertyRest || checkRestProperty) {
/** @type {[string, import('comment-parser').Spec][]} */
const extraProperties = [];
for (const [
idx,
name,
] of actualNames.entries()) {
const match = name.startsWith(tag.name.trim() + '.');
if (
match && !expectedNames.some(
utils.comparePaths(name),
) && !utils.comparePaths(name)(tag.name) &&
(!disableExtraPropertyReporting || properties.some((prop) => {
return prop.split('.').length >= name.split('.').length - 1;
}))
) {
extraProperties.push([
name, paramTags[idx][1],
]);
}
}
if (extraProperties.length) {
for (const [
extraProperty,
tg,
] of extraProperties) {
report(`@${targetTagName} "${extraProperty}" does not exist on ${tag.name}`, null, tg);
}
return true;
}
}
return hasMissing;
}
let funcParamName;
if (typeof functionParameterName === 'object') {
const {
name,
} = functionParameterName;
funcParamName = name;
} else {
funcParamName = functionParameterName;
}
if (funcParamName !== tag.name.trim()) {
// Todo: Improve for array or object child items
const actualNames = paramTagsNonNested.map(([
, {
name,
},
]) => {
return name.trim();
});
const expectedNames = functionParameterNames.map((item, idx) => {
if (/**
* @type {[string|undefined, (import('../jsdocUtils.js').FlattendRootInfo & {
* annotationParamName?: string,
})]} */ (item)?.[1]?.names) {
return actualNames[idx];
}
return item;
}).filter((item) => {
return item !== 'this';
});
// When disableMissingParamChecks is true tag names can be omitted.
// Report when the tag names do not match the expected names or they are used out of order.
if (disableMissingParamChecks) {
const usedExpectedNames = expectedNames.map(a => a?.toString()).filter(expectedName => expectedName && actualNames.includes(expectedName));
const usedInOrder = actualNames.every((actualName, idx) => actualName === usedExpectedNames[idx]);
if (usedInOrder) {
return false;
}
}
report(
`Expected @${targetTagName} names to be "${
expectedNames.map((expectedName) => {
return typeof expectedName === 'object' &&
'name' in expectedName &&
expectedName.restElement
? '...' + expectedName.name
: expectedName;
}).join(', ')
}". Got "${actualNames.join(', ')}".`,
null,
tag,
);
return true;
}
return false;
});
};
/**
* @param {string} targetTagName
* @param {boolean} _allowExtraTrailingParamDocs
* @param {{
* name: string,
* idx: import('../iterateJsdoc.js').Integer
* }[]} jsdocParameterNames
* @param {import('comment-parser').Block} jsdoc
* @param {Function} report
* @returns {boolean}
*/
const validateParameterNamesDeep = (
targetTagName, _allowExtraTrailingParamDocs,
jsdocParameterNames, jsdoc, report,
) => {
/** @type {string} */
let lastRealParameter;
return jsdocParameterNames.some(({
name: jsdocParameterName,
idx,
}) => {
const isPropertyPath = jsdocParameterName.includes('.');
if (isPropertyPath) {
if (!lastRealParameter) {
report(`@${targetTagName} path declaration ("${jsdocParameterName}") appears before any real parameter.`, null, jsdoc.tags[idx]);
return true;
}
let pathRootNodeName = jsdocParameterName.slice(0, jsdocParameterName.indexOf('.'));
if (pathRootNodeName.endsWith('[]')) {
pathRootNodeName = pathRootNodeName.slice(0, -2);
}
if (pathRootNodeName !== lastRealParameter) {
report(
`@${targetTagName} path declaration ("${jsdocParameterName}") root node name ("${pathRootNodeName}") ` +
`does not match previous real parameter name ("${lastRealParameter}").`,
null,
jsdoc.tags[idx],
);
return true;
}
} else {
lastRealParameter = jsdocParameterName;
}
return false;
});
};
export default iterateJsdoc(({
context,
jsdoc,
report,
utils,
}) => {
const {
allowExtraTrailingParamDocs,
checkDestructured = true,
checkRestProperty = false,
checkTypesPattern = '/^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$/',
enableFixer = false,
useDefaultObjectProperties = false,
disableExtraPropertyReporting = false,
disableMissingParamChecks = false,
} = context.options[0] || {};
const checkTypesRegex = utils.getRegexFromString(checkTypesPattern);
const jsdocParameterNamesDeep = utils.getJsdocTagsDeep('param');
if (!jsdocParameterNamesDeep || !jsdocParameterNamesDeep.length) {
return;
}
const functionParameterNames = utils.getFunctionParameterNames(useDefaultObjectProperties);
const targetTagName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'param',
}));
const isError = validateParameterNames(
targetTagName,
allowExtraTrailingParamDocs,
checkDestructured,
checkRestProperty,
checkTypesRegex,
disableExtraPropertyReporting,
disableMissingParamChecks,
enableFixer,
functionParameterNames,
jsdoc,
utils,
report,
);
if (isError || !checkDestructured) {
return;
}
validateParameterNamesDeep(
targetTagName, allowExtraTrailingParamDocs, jsdocParameterNamesDeep, jsdoc, report,
);
}, {
contextDefaults: [
'ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression', 'TSDeclareFunction',
// Add this to above defaults
'TSMethodSignature'
],
meta: {
docs: {
description: 'Ensures that parameter names in JSDoc match those in the function declaration.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-param-names.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
allowExtraTrailingParamDocs: {
type: 'boolean',
},
checkDestructured: {
type: 'boolean',
},
checkRestProperty: {
type: 'boolean',
},
checkTypesPattern: {
type: 'string',
},
disableExtraPropertyReporting: {
type: 'boolean',
},
disableMissingParamChecks: {
type: 'boolean',
},
enableFixer: {
type: 'boolean',
},
useDefaultObjectProperties: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,152 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {string} targetTagName
* @param {boolean} enableFixer
* @param {import('comment-parser').Block} jsdoc
* @param {import('../iterateJsdoc.js').Utils} utils
* @returns {boolean}
*/
const validatePropertyNames = (
targetTagName,
enableFixer,
jsdoc, utils,
) => {
const propertyTags = Object.entries(jsdoc.tags).filter(([
, tag,
]) => {
return tag.tag === targetTagName;
});
return propertyTags.some(([
, tag,
], index) => {
/** @type {import('../iterateJsdoc.js').Integer} */
let tagsIndex;
const dupeTagInfo = propertyTags.find(([
tgsIndex,
tg,
], idx) => {
tagsIndex = Number(tgsIndex);
return tg.name === tag.name && idx !== index;
});
if (dupeTagInfo) {
utils.reportJSDoc(`Duplicate @${targetTagName} "${tag.name}"`, dupeTagInfo[1], enableFixer ? () => {
utils.removeTag(tagsIndex);
} : null);
return true;
}
return false;
});
};
/**
* @param {string} targetTagName
* @param {{
* idx: number;
* name: string;
* type: string;
* }[]} jsdocPropertyNames
* @param {import('comment-parser').Block} jsdoc
* @param {Function} report
*/
const validatePropertyNamesDeep = (
targetTagName,
jsdocPropertyNames, jsdoc, report,
) => {
/** @type {string} */
let lastRealProperty;
return jsdocPropertyNames.some(({
name: jsdocPropertyName,
idx,
}) => {
const isPropertyPath = jsdocPropertyName.includes('.');
if (isPropertyPath) {
if (!lastRealProperty) {
report(`@${targetTagName} path declaration ("${jsdocPropertyName}") appears before any real property.`, null, jsdoc.tags[idx]);
return true;
}
let pathRootNodeName = jsdocPropertyName.slice(0, jsdocPropertyName.indexOf('.'));
if (pathRootNodeName.endsWith('[]')) {
pathRootNodeName = pathRootNodeName.slice(0, -2);
}
if (pathRootNodeName !== lastRealProperty) {
report(
`@${targetTagName} path declaration ("${jsdocPropertyName}") root node name ("${pathRootNodeName}") ` +
`does not match previous real property name ("${lastRealProperty}").`,
null,
jsdoc.tags[idx],
);
return true;
}
} else {
lastRealProperty = jsdocPropertyName;
}
return false;
});
};
export default iterateJsdoc(({
context,
jsdoc,
report,
utils,
}) => {
const {
enableFixer = false,
} = context.options[0] || {};
const jsdocPropertyNamesDeep = utils.getJsdocTagsDeep('property');
if (!jsdocPropertyNamesDeep || !jsdocPropertyNamesDeep.length) {
return;
}
const targetTagName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'property',
}));
const isError = validatePropertyNames(
targetTagName,
enableFixer,
jsdoc,
utils,
);
if (isError) {
return;
}
validatePropertyNamesDeep(
targetTagName, jsdocPropertyNamesDeep, jsdoc, report,
);
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Ensures that property names in JSDoc are not duplicated on the same block and that nested properties have defined roots.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-property-names.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
enableFixer: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,30 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
jsdoc,
report,
settings,
}) => {
const {
mode,
} = settings;
// Don't check for "permissive" and "closure"
if (mode === 'jsdoc' || mode === 'typescript') {
for (const tag of jsdoc.tags) {
if (tag.type.slice(-1) === '=') {
report('Syntax should not be Google Closure Compiler style.', null, tag);
break;
}
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports against syntax not valid for the mode (e.g., Google Closure Compiler in non-Closure mode).',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-syntax.md#repos-sticky-header',
},
type: 'suggestion',
},
});

View File

@@ -0,0 +1,314 @@
import iterateJsdoc from '../iterateJsdoc.js';
import escapeStringRegexp from 'escape-string-regexp';
// https://babeljs.io/docs/en/babel-plugin-transform-react-jsx/
const jsxTagNames = new Set([
'jsx',
'jsxFrag',
'jsxImportSource',
'jsxRuntime',
]);
const typedTagsAlwaysUnnecessary = new Set([
'augments',
'callback',
'class',
'enum',
'implements',
'private',
'property',
'protected',
'public',
'readonly',
'this',
'type',
'typedef',
]);
const typedTagsNeedingName = new Set([
'template',
]);
const typedTagsUnnecessaryOutsideDeclare = new Set([
'abstract',
'access',
'class',
'constant',
'constructs',
'default',
'enum',
'export',
'exports',
'function',
'global',
'inherits',
'instance',
'interface',
'member',
'memberof',
'memberOf',
'method',
'mixes',
'mixin',
'module',
'name',
'namespace',
'override',
'property',
'requires',
'static',
'this',
]);
export default iterateJsdoc(({
sourceCode,
jsdoc,
report,
utils,
context,
node,
settings,
jsdocNode,
}) => {
const
/**
* @type {{
* definedTags: string[],
* enableFixer: boolean,
* jsxTags: boolean,
* typed: boolean
}} */ {
definedTags = [],
enableFixer = true,
jsxTags,
typed,
} = context.options[0] || {};
/** @type {(string|undefined)[]} */
let definedPreferredTags = [];
const {
tagNamePreference,
structuredTags,
} = settings;
const definedStructuredTags = Object.keys(structuredTags);
const definedNonPreferredTags = Object.keys(tagNamePreference);
if (definedNonPreferredTags.length) {
definedPreferredTags = Object.values(tagNamePreference).map((preferredTag) => {
if (typeof preferredTag === 'string') {
// May become an empty string but will be filtered out below
return preferredTag;
}
if (!preferredTag) {
return undefined;
}
if (typeof preferredTag !== 'object') {
utils.reportSettings(
'Invalid `settings.jsdoc.tagNamePreference`. Values must be falsy, a string, or an object.',
);
}
return preferredTag.replacement;
})
.filter(Boolean);
}
/**
* @param {import('eslint').Rule.Node} subNode
* @returns {boolean}
*/
const isInAmbientContext = (subNode) => {
return subNode.type === 'Program' ?
context.getFilename().endsWith('.d.ts') :
Boolean(
/** @type {import('@typescript-eslint/types').TSESTree.VariableDeclaration} */ (
subNode
).declare,
) || isInAmbientContext(subNode.parent);
};
/**
* @param {import('comment-parser').Spec} jsdocTag
* @returns {boolean}
*/
const tagIsRedundantWhenTyped = (jsdocTag) => {
if (!typedTagsUnnecessaryOutsideDeclare.has(jsdocTag.tag)) {
return false;
}
if (jsdocTag.tag === 'default') {
return false;
}
if (node === null) {
return false;
}
if (context.getFilename().endsWith('.d.ts') && [
'Program', null, undefined,
].includes(node?.parent?.type)) {
return false;
}
if (isInAmbientContext(/** @type {import('eslint').Rule.Node} */ (node))) {
return false;
}
return true;
};
/**
* @param {string} message
* @param {import('comment-parser').Spec} jsdocTag
* @param {import('../iterateJsdoc.js').Integer} tagIndex
* @param {Partial<import('comment-parser').Tokens>} [additionalTagChanges]
* @returns {void}
*/
const reportWithTagRemovalFixer = (message, jsdocTag, tagIndex, additionalTagChanges) => {
utils.reportJSDoc(message, jsdocTag, enableFixer ? () => {
if (jsdocTag.description.trim()) {
utils.changeTag(jsdocTag, {
postType: '',
type: '',
...additionalTagChanges,
});
} else {
utils.removeTag(tagIndex, {
removeEmptyBlock: true,
});
}
} : null, true);
};
/**
* @param {import('comment-parser').Spec} jsdocTag
* @param {import('../iterateJsdoc.js').Integer} tagIndex
* @returns {boolean}
*/
const checkTagForTypedValidity = (jsdocTag, tagIndex) => {
if (typedTagsAlwaysUnnecessary.has(jsdocTag.tag)) {
reportWithTagRemovalFixer(
`'@${jsdocTag.tag}' is redundant when using a type system.`,
jsdocTag,
tagIndex,
{
postTag: '',
tag: '',
},
);
return true;
}
if (tagIsRedundantWhenTyped(jsdocTag)) {
reportWithTagRemovalFixer(
`'@${jsdocTag.tag}' is redundant outside of ambient (\`declare\`/\`.d.ts\`) contexts when using a type system.`,
jsdocTag,
tagIndex,
);
return true;
}
if (typedTagsNeedingName.has(jsdocTag.tag) && !jsdocTag.name) {
reportWithTagRemovalFixer(
`'@${jsdocTag.tag}' without a name is redundant when using a type system.`,
jsdocTag,
tagIndex,
);
return true;
}
return false;
};
for (let tagIndex = 0; tagIndex < jsdoc.tags.length; tagIndex += 1) {
const jsdocTag = jsdoc.tags[tagIndex];
const tagName = jsdocTag.tag;
if (jsxTags && jsxTagNames.has(tagName)) {
continue;
}
if (typed && checkTagForTypedValidity(jsdocTag, tagIndex)) {
continue;
}
const validTags = [
...definedTags,
...(/** @type {string[]} */ (definedPreferredTags)),
...definedNonPreferredTags,
...definedStructuredTags,
...typed ? typedTagsNeedingName : [],
];
if (utils.isValidTag(tagName, validTags)) {
let preferredTagName = utils.getPreferredTagName({
allowObjectReturn: true,
defaultMessage: `Blacklisted tag found (\`@${tagName}\`)`,
tagName,
});
if (!preferredTagName) {
continue;
}
let message;
if (typeof preferredTagName === 'object') {
({
message,
replacement: preferredTagName,
} = /** @type {{message: string; replacement?: string | undefined;}} */ (
preferredTagName
));
}
if (!message) {
message = `Invalid JSDoc tag (preference). Replace "${tagName}" JSDoc tag with "${preferredTagName}".`;
}
if (preferredTagName !== tagName) {
report(message, (fixer) => {
const replacement = sourceCode.getText(jsdocNode).replace(
new RegExp(`@${escapeStringRegexp(tagName)}\\b`, 'u'),
`@${preferredTagName}`,
);
return fixer.replaceText(jsdocNode, replacement);
}, jsdocTag);
}
} else {
report(`Invalid JSDoc tag name "${tagName}".`, null, jsdocTag);
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports invalid block tag names.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-tag-names.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
definedTags: {
items: {
type: 'string',
},
type: 'array',
},
enableFixer: {
type: 'boolean',
},
jsxTags: {
type: 'boolean',
},
typed: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,172 @@
import {
parse as parseType,
traverse,
tryParse as tryParseType,
} from '@es-joy/jsdoccomment';
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
utils,
node,
settings,
report,
}) => {
const {
mode
} = settings;
const templateTags = utils.getTags('template');
const usedNames = new Set();
/**
* @param {string} potentialType
*/
const checkForUsedTypes = (potentialType) => {
let parsedType;
try {
parsedType = mode === 'permissive' ?
tryParseType(/** @type {string} */ (potentialType)) :
parseType(/** @type {string} */ (potentialType), mode);
} catch {
return;
}
traverse(parsedType, (nde) => {
const {
type,
value,
} = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde);
if (type === 'JsdocTypeName' && (/^[A-Z]$/).test(value)) {
usedNames.add(value);
}
});
};
const checkParamsAndReturnsTags = () => {
const paramName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'param',
}));
const paramTags = utils.getTags(paramName);
for (const paramTag of paramTags) {
checkForUsedTypes(paramTag.type);
}
const returnsName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'returns',
}));
const returnsTags = utils.getTags(returnsName);
for (const returnsTag of returnsTags) {
checkForUsedTypes(returnsTag.type);
}
};
const checkTemplateTags = () => {
for (const tag of templateTags) {
const {name} = tag;
const names = name.split(/,\s*/);
for (const name of names) {
if (!usedNames.has(name)) {
report(`@template ${name} not in use`, null, tag);
}
}
}
};
/**
* @param {import('@typescript-eslint/types').TSESTree.FunctionDeclaration|
* import('@typescript-eslint/types').TSESTree.ClassDeclaration|
* import('@typescript-eslint/types').TSESTree.TSInterfaceDeclaration|
* import('@typescript-eslint/types').TSESTree.TSTypeAliasDeclaration} aliasDeclaration
* @param {boolean} [checkParamsAndReturns]
*/
const checkParameters = (aliasDeclaration, checkParamsAndReturns) => {
/* c8 ignore next -- Guard */
const {params} = aliasDeclaration.typeParameters ?? {params: []};
for (const {name: {name}} of params) {
usedNames.add(name);
}
if (checkParamsAndReturns) {
checkParamsAndReturnsTags();
}
checkTemplateTags();
};
const handleTypeAliases = () => {
const nde = /** @type {import('@typescript-eslint/types').TSESTree.Node} */ (
node
);
if (!nde) {
return;
}
switch (nde.type) {
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
switch (nde.declaration?.type) {
case 'FunctionDeclaration':
checkParameters(nde.declaration, true);
break;
case 'ClassDeclaration':
case 'TSTypeAliasDeclaration':
case 'TSInterfaceDeclaration':
checkParameters(nde.declaration);
break;
}
break;
case 'FunctionDeclaration':
checkParameters(nde, true);
break;
case 'ClassDeclaration':
case 'TSTypeAliasDeclaration':
case 'TSInterfaceDeclaration':
checkParameters(nde);
break;
}
};
const callbackTags = utils.getTags('callback');
const functionTags = utils.getTags('function');
if (callbackTags.length || functionTags.length) {
checkParamsAndReturnsTags();
checkTemplateTags();
return;
}
const typedefTags = utils.getTags('typedef');
if (!typedefTags.length || typedefTags.length >= 2) {
handleTypeAliases();
return;
}
const potentialTypedefType = typedefTags[0].type;
checkForUsedTypes(potentialTypedefType);
const propertyName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'property',
}));
const propertyTags = utils.getTags(propertyName);
for (const propertyTag of propertyTags) {
checkForUsedTypes(propertyTag.type);
}
for (const tag of templateTags) {
const {name} = tag;
const names = name.split(/,\s*/);
for (const name of names) {
if (!usedNames.has(name)) {
report(`@template ${name} not in use`, null, tag);
}
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Checks that any `@template` names are actually used in the connected `@typedef` or type alias.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-template.md#repos-sticky-header',
},
schema: [],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,535 @@
import iterateJsdoc from '../iterateJsdoc.js';
import {
parse,
stringify,
traverse,
tryParse,
} from '@es-joy/jsdoccomment';
const strictNativeTypes = [
'undefined',
'null',
'boolean',
'number',
'bigint',
'string',
'symbol',
'object',
'Array',
'Function',
'Date',
'RegExp',
];
/**
* Adjusts the parent type node `meta` for generic matches (or type node
* `type` for `JsdocTypeAny`) and sets the type node `value`.
* @param {string} type The actual type
* @param {string} preferred The preferred type
* @param {boolean} isGenericMatch
* @param {string} typeNodeName
* @param {import('jsdoc-type-pratt-parser').NonRootResult} node
* @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
* @returns {void}
*/
const adjustNames = (type, preferred, isGenericMatch, typeNodeName, node, parentNode) => {
let ret = preferred;
if (isGenericMatch) {
const parentMeta = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ (
parentNode
).meta;
if (preferred === '[]') {
parentMeta.brackets = 'square';
parentMeta.dot = false;
ret = 'Array';
} else {
const dotBracketEnd = preferred.match(/\.(?:<>)?$/u);
if (dotBracketEnd) {
parentMeta.brackets = 'angle';
parentMeta.dot = true;
ret = preferred.slice(0, -dotBracketEnd[0].length);
} else {
const bracketEnd = preferred.endsWith('<>');
if (bracketEnd) {
parentMeta.brackets = 'angle';
parentMeta.dot = false;
ret = preferred.slice(0, -2);
} else if (
parentMeta?.brackets === 'square' &&
(typeNodeName === '[]' || typeNodeName === 'Array')
) {
parentMeta.brackets = 'angle';
parentMeta.dot = false;
}
}
}
} else if (type === 'JsdocTypeAny') {
node.type = 'JsdocTypeName';
}
/** @type {import('jsdoc-type-pratt-parser').NameResult} */ (
node
).value = ret.replace(/(?:\.|<>|\.<>|\[\])$/u, '');
// For bare pseudo-types like `<>`
if (!ret) {
/** @type {import('jsdoc-type-pratt-parser').NameResult} */ (
node
).value = typeNodeName;
}
};
/**
* @param {boolean} [upperCase]
* @returns {string}
*/
const getMessage = (upperCase) => {
return 'Use object shorthand or index signatures instead of ' +
'`' + (upperCase ? 'O' : 'o') + 'bject`, e.g., `{[key: string]: string}`';
};
export default iterateJsdoc(({
jsdocNode,
sourceCode,
report,
utils,
settings,
context,
}) => {
const jsdocTagsWithPossibleType = utils.filterTags((tag) => {
return Boolean(utils.tagMightHaveTypePosition(tag.tag));
});
const
/**
* @type {{
* preferredTypes: import('../iterateJsdoc.js').PreferredTypes,
* structuredTags: import('../iterateJsdoc.js').StructuredTags,
* mode: import('../jsdocUtils.js').ParserMode
* }}
*/
{
preferredTypes: preferredTypesOriginal,
structuredTags,
mode,
} = settings;
const injectObjectPreferredTypes = !('Object' in preferredTypesOriginal ||
'object' in preferredTypesOriginal ||
'object.<>' in preferredTypesOriginal ||
'Object.<>' in preferredTypesOriginal ||
'object<>' in preferredTypesOriginal);
/**
* @type {{
* message: string,
* replacement: false
* }}
*/
const info = {
message: getMessage(),
replacement: false,
};
/**
* @type {{
* message: string,
* replacement: false
* }}
*/
const infoUC = {
message: getMessage(true),
replacement: false,
};
/** @type {import('../iterateJsdoc.js').PreferredTypes} */
const typeToInject = mode === 'typescript' ?
{
Object: 'object',
'object.<>': info,
'Object.<>': infoUC,
'object<>': info,
'Object<>': infoUC,
} :
{
Object: 'object',
'object.<>': 'Object<>',
'Object.<>': 'Object<>',
'object<>': 'Object<>',
};
/** @type {import('../iterateJsdoc.js').PreferredTypes} */
const preferredTypes = {
...injectObjectPreferredTypes ?
typeToInject :
{},
...preferredTypesOriginal,
};
const
/**
* @type {{
* noDefaults: boolean,
* unifyParentAndChildTypeChecks: boolean,
* exemptTagContexts: ({
* tag: string,
* types: true|string[]
* })[]
* }}
*/ {
noDefaults,
unifyParentAndChildTypeChecks,
exemptTagContexts = [],
} = context.options[0] || {};
/**
* Gets information about the preferred type: whether there is a matching
* preferred type, what the type is, and whether it is a match to a generic.
* @param {string} _type Not currently in use
* @param {string} typeNodeName
* @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
* @param {string|undefined} property
* @returns {[hasMatchingPreferredType: boolean, typeName: string, isGenericMatch: boolean]}
*/
const getPreferredTypeInfo = (_type, typeNodeName, parentNode, property) => {
let hasMatchingPreferredType = false;
let isGenericMatch = false;
let typeName = typeNodeName;
const isNameOfGeneric = parentNode !== undefined && parentNode.type === 'JsdocTypeGeneric' && property === 'left';
if (unifyParentAndChildTypeChecks || isNameOfGeneric) {
const brackets = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ (
parentNode
)?.meta?.brackets;
const dot = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */ (
parentNode
)?.meta?.dot;
if (brackets === 'angle') {
const checkPostFixes = dot ? [
'.', '.<>',
] : [
'<>',
];
isGenericMatch = checkPostFixes.some((checkPostFix) => {
if (preferredTypes?.[typeNodeName + checkPostFix] !== undefined) {
typeName += checkPostFix;
return true;
}
return false;
});
}
if (
!isGenericMatch && property &&
/** @type {import('jsdoc-type-pratt-parser').NonRootResult} */ (
parentNode
).type === 'JsdocTypeGeneric'
) {
const checkPostFixes = dot ? [
'.', '.<>',
] : [
brackets === 'angle' ? '<>' : '[]',
];
isGenericMatch = checkPostFixes.some((checkPostFix) => {
if (preferredTypes?.[checkPostFix] !== undefined) {
typeName = checkPostFix;
return true;
}
return false;
});
}
}
const directNameMatch = preferredTypes?.[typeNodeName] !== undefined &&
!Object.values(preferredTypes).includes(typeNodeName);
const unifiedSyntaxParentMatch = property && directNameMatch && unifyParentAndChildTypeChecks;
isGenericMatch = isGenericMatch || Boolean(unifiedSyntaxParentMatch);
hasMatchingPreferredType = isGenericMatch ||
directNameMatch && !property;
return [
hasMatchingPreferredType, typeName, isGenericMatch,
];
};
/**
* Iterates strict types to see if any should be added to `invalidTypes` (and
* the the relevant strict type returned as the new preferred type).
* @param {string} typeNodeName
* @param {string|undefined} preferred
* @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
* @param {(string|false|undefined)[][]} invalidTypes
* @returns {string|undefined} The `preferred` type string, optionally changed
*/
const checkNativeTypes = (typeNodeName, preferred, parentNode, invalidTypes) => {
let changedPreferred = preferred;
for (const strictNativeType of strictNativeTypes) {
if (
strictNativeType === 'object' &&
(
// This is not set to remap with exact type match (e.g.,
// `object: 'Object'`), so can ignore (including if circular)
!preferredTypes?.[typeNodeName] ||
// Although present on `preferredTypes` for remapping, this is a
// parent object without a parent match (and not
// `unifyParentAndChildTypeChecks`) and we don't want
// `object<>` given TypeScript issue https://github.com/microsoft/TypeScript/issues/20555
/**
* @type {import('jsdoc-type-pratt-parser').GenericResult}
*/
(
parentNode
)?.elements?.length && (
/**
* @type {import('jsdoc-type-pratt-parser').GenericResult}
*/
(
parentNode
)?.left?.type === 'JsdocTypeName' &&
/**
* @type {import('jsdoc-type-pratt-parser').GenericResult}
*/
(parentNode)?.left?.value === 'Object'
)
)
) {
continue;
}
if (strictNativeType !== typeNodeName &&
strictNativeType.toLowerCase() === typeNodeName.toLowerCase() &&
// Don't report if user has own map for a strict native type
(!preferredTypes || preferredTypes?.[strictNativeType] === undefined)
) {
changedPreferred = strictNativeType;
invalidTypes.push([
typeNodeName, changedPreferred,
]);
break;
}
}
return changedPreferred;
};
/**
* Collect invalid type info.
* @param {string} type
* @param {string} value
* @param {string} tagName
* @param {string} nameInTag
* @param {number} idx
* @param {string|undefined} property
* @param {import('jsdoc-type-pratt-parser').NonRootResult} node
* @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
* @param {(string|false|undefined)[][]} invalidTypes
* @returns {void}
*/
const getInvalidTypes = (type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes) => {
let typeNodeName = type === 'JsdocTypeAny' ? '*' : value;
const [
hasMatchingPreferredType,
typeName,
isGenericMatch,
] = getPreferredTypeInfo(type, typeNodeName, parentNode, property);
let preferred;
let types;
if (hasMatchingPreferredType) {
const preferredSetting = preferredTypes[typeName];
typeNodeName = typeName === '[]' ? typeName : typeNodeName;
if (!preferredSetting) {
invalidTypes.push([
typeNodeName,
]);
} else if (typeof preferredSetting === 'string') {
preferred = preferredSetting;
invalidTypes.push([
typeNodeName, preferred,
]);
} else if (preferredSetting && typeof preferredSetting === 'object') {
const nextItem = preferredSetting.skipRootChecking && jsdocTagsWithPossibleType[idx + 1];
if (!nextItem || !nextItem.name.startsWith(`${nameInTag}.`)) {
preferred = preferredSetting.replacement;
invalidTypes.push([
typeNodeName,
preferred,
preferredSetting.message,
]);
}
} else {
utils.reportSettings(
'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.',
);
return;
}
} else if (Object.entries(structuredTags).some(([
tag,
{
type: typs,
},
]) => {
types = typs;
return tag === tagName &&
Array.isArray(types) &&
!types.includes(typeNodeName);
})) {
invalidTypes.push([
typeNodeName, types,
]);
} else if (!noDefaults && type === 'JsdocTypeName') {
preferred = checkNativeTypes(typeNodeName, preferred, parentNode, invalidTypes);
}
// For fixer
if (preferred) {
adjustNames(type, preferred, isGenericMatch, typeNodeName, node, parentNode);
}
};
for (const [
idx,
jsdocTag,
] of jsdocTagsWithPossibleType.entries()) {
/** @type {(string|false|undefined)[][]} */
const invalidTypes = [];
let typeAst;
try {
typeAst = mode === 'permissive' ? tryParse(jsdocTag.type) : parse(jsdocTag.type, mode);
} catch {
continue;
}
const {
tag: tagName,
name: nameInTag,
} = jsdocTag;
traverse(typeAst, (node, parentNode, property) => {
const {
type,
value,
} =
/**
* @type {import('jsdoc-type-pratt-parser').NameResult}
*/ (node);
if (![
'JsdocTypeName', 'JsdocTypeAny',
].includes(type)) {
return;
}
getInvalidTypes(type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes);
});
if (invalidTypes.length) {
const fixedType = stringify(typeAst);
/**
* @type {import('eslint').Rule.ReportFixer}
*/
const fix = (fixer) => {
return fixer.replaceText(
jsdocNode,
sourceCode.getText(jsdocNode).replace(
`{${jsdocTag.type}}`,
`{${fixedType}}`,
),
);
};
for (const [
badType,
preferredType = '',
msg,
] of invalidTypes) {
const tagValue = jsdocTag.name ? ` "${jsdocTag.name}"` : '';
if (exemptTagContexts.some(({
tag,
types,
}) => {
return tag === tagName &&
(types === true || types.includes(jsdocTag.type));
})) {
continue;
}
report(
msg ||
`Invalid JSDoc @${tagName}${tagValue} type "${badType}"` +
(preferredType ? '; ' : '.') +
(preferredType ? `prefer: ${JSON.stringify(preferredType)}.` : ''),
preferredType ? fix : null,
jsdocTag,
msg ? {
tagName,
tagValue,
} : undefined,
);
}
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports invalid types.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-types.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
exemptTagContexts: {
items: {
additionalProperties: false,
properties: {
tag: {
type: 'string',
},
types: {
oneOf: [
{
type: 'boolean',
},
{
items: {
type: 'string',
},
type: 'array',
},
],
},
},
type: 'object',
},
type: 'array',
},
noDefaults: {
type: 'boolean',
},
unifyParentAndChildTypeChecks: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,249 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createSyncFn } from 'synckit';
import semver from 'semver';
import spdxExpressionParse from 'spdx-expression-parse';
import iterateJsdoc from '../iterateJsdoc.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pathName = join(__dirname, '../import-worker.mjs');
const allowedKinds = new Set([
'class',
'constant',
'event',
'external',
'file',
'function',
'member',
'mixin',
'module',
'namespace',
'typedef',
]);
export default iterateJsdoc(({
utils,
report,
context,
settings,
}) => {
const options = context.options[0] || {};
const {
allowedLicenses = null,
allowedAuthors = null,
numericOnlyVariation = false,
licensePattern = '/([^\n\r]*)/gu',
} = options;
utils.forEachPreferredTag('version', (jsdocParameter, targetTagName) => {
const version = /** @type {string} */ (
utils.getTagDescription(jsdocParameter)
).trim();
if (!version) {
report(
`Missing JSDoc @${targetTagName} value.`,
null,
jsdocParameter,
);
} else if (!semver.valid(version)) {
report(
`Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}".`,
null,
jsdocParameter,
);
}
});
utils.forEachPreferredTag('kind', (jsdocParameter, targetTagName) => {
const kind = /** @type {string} */ (
utils.getTagDescription(jsdocParameter)
).trim();
if (!kind) {
report(
`Missing JSDoc @${targetTagName} value.`,
null,
jsdocParameter,
);
} else if (!allowedKinds.has(kind)) {
report(
`Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}"; ` +
`must be one of: ${[
...allowedKinds,
].join(', ')}.`,
null,
jsdocParameter,
);
}
});
if (numericOnlyVariation) {
utils.forEachPreferredTag('variation', (jsdocParameter, targetTagName) => {
const variation = /** @type {string} */ (
utils.getTagDescription(jsdocParameter)
).trim();
if (!variation) {
report(
`Missing JSDoc @${targetTagName} value.`,
null,
jsdocParameter,
);
} else if (
!Number.isInteger(Number(variation)) ||
Number(variation) <= 0
) {
report(
`Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}".`,
null,
jsdocParameter,
);
}
});
}
utils.forEachPreferredTag('since', (jsdocParameter, targetTagName) => {
const version = /** @type {string} */ (
utils.getTagDescription(jsdocParameter)
).trim();
if (!version) {
report(
`Missing JSDoc @${targetTagName} value.`,
null,
jsdocParameter,
);
} else if (!semver.valid(version)) {
report(
`Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}".`,
null,
jsdocParameter,
);
}
});
utils.forEachPreferredTag('license', (jsdocParameter, targetTagName) => {
const licenseRegex = utils.getRegexFromString(licensePattern, 'g');
const matches = /** @type {string} */ (
utils.getTagDescription(jsdocParameter)
).matchAll(licenseRegex);
let positiveMatch = false;
for (const match of matches) {
const license = match[1] || match[0];
if (license) {
positiveMatch = true;
}
if (!license.trim()) {
// Avoid reporting again as empty match
if (positiveMatch) {
return;
}
report(
`Missing JSDoc @${targetTagName} value.`,
null,
jsdocParameter,
);
} else if (allowedLicenses) {
if (allowedLicenses !== true && !allowedLicenses.includes(license)) {
report(
`Invalid JSDoc @${targetTagName}: "${license}"; expected one of ${allowedLicenses.join(', ')}.`,
null,
jsdocParameter,
);
}
} else {
try {
spdxExpressionParse(license);
} catch {
report(
`Invalid JSDoc @${targetTagName}: "${license}"; expected SPDX expression: https://spdx.org/licenses/.`,
null,
jsdocParameter,
);
}
}
}
});
if (settings.mode === 'typescript') {
utils.forEachPreferredTag('import', (tag) => {
const {
type, name, description
} = tag;
const typePart = type ? `{${type}} `: '';
const imprt = 'import ' + (description
? `${typePart}${name} ${description}`
: `${typePart}${name}`);
const getImports = createSyncFn(pathName);
if (!getImports(imprt)) {
report(
`Bad @import tag`,
null,
tag,
);
}
});
}
utils.forEachPreferredTag('author', (jsdocParameter, targetTagName) => {
const author = /** @type {string} */ (
utils.getTagDescription(jsdocParameter)
).trim();
if (!author) {
report(
`Missing JSDoc @${targetTagName} value.`,
null,
jsdocParameter,
);
} else if (allowedAuthors && !allowedAuthors.includes(author)) {
report(
`Invalid JSDoc @${targetTagName}: "${utils.getTagDescription(jsdocParameter)}"; expected one of ${allowedAuthors.join(', ')}.`,
null,
jsdocParameter,
);
}
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'This rule checks the values for a handful of tags: `@version`, `@since`, `@license` and `@author`.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-values.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
allowedAuthors: {
items: {
type: 'string',
},
type: 'array',
},
allowedLicenses: {
anyOf: [
{
items: {
type: 'string',
},
type: 'array',
},
{
type: 'boolean',
},
],
},
licensePattern: {
type: 'string',
},
numericOnlyVariation: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,388 @@
import iterateJsdoc from '../iterateJsdoc.js';
import {
getSettings,
} from '../iterateJsdoc.js';
import {
getIndent,
getContextObject,
enforcedContexts,
} from '../jsdocUtils.js';
import {
getNonJsdocComment,
getDecorator,
getReducedASTNode,
getFollowingComment,
} from '@es-joy/jsdoccomment';
/** @type {import('eslint').Rule.RuleModule} */
export default {
create (context) {
/**
* @typedef {import('eslint').AST.Token | import('estree').Comment | {
* type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
* range: [number, number],
* value: string
* }} Token
*/
/**
* @callback AddComment
* @param {boolean|undefined} inlineCommentBlock
* @param {Token} comment
* @param {string} indent
* @param {number} lines
* @param {import('eslint').Rule.RuleFixer} fixer
*/
/* c8 ignore next -- Fallback to deprecated method */
const {
sourceCode = context.getSourceCode(),
} = context;
const settings = getSettings(context);
if (!settings) {
return {};
}
const {
contexts = settings.contexts || [],
contextsAfter = /** @type {string[]} */ ([]),
contextsBeforeAndAfter = [
'VariableDeclarator', 'TSPropertySignature', 'PropertyDefinition'
],
enableFixer = true,
enforceJsdocLineStyle = 'multi',
lineOrBlockStyle = 'both',
allowedPrefixes = ['@ts-', 'istanbul ', 'c8 ', 'v8 ', 'eslint', 'prettier-']
} = context.options[0] ?? {};
let reportingNonJsdoc = false;
/**
* @param {string} messageId
* @param {import('estree').Comment|Token} comment
* @param {import('eslint').Rule.Node} node
* @param {import('eslint').Rule.ReportFixer} fixer
*/
const report = (messageId, comment, node, fixer) => {
const loc = {
end: {
column: 0,
/* c8 ignore next 2 -- Guard */
// @ts-expect-error Ok
line: (comment.loc?.start?.line ?? 1),
},
start: {
column: 0,
/* c8 ignore next 2 -- Guard */
// @ts-expect-error Ok
line: (comment.loc?.start?.line ?? 1)
},
};
context.report({
fix: enableFixer ? fixer : null,
loc,
messageId,
node,
});
};
/**
* @param {import('eslint').Rule.Node} node
* @param {import('eslint').AST.Token | import('estree').Comment | {
* type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
* range: [number, number],
* value: string
* }} comment
* @param {AddComment} addComment
* @param {import('../iterateJsdoc.js').Context[]} ctxts
*/
const getFixer = (node, comment, addComment, ctxts) => {
return /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
// Default to one line break if the `minLines`/`maxLines` settings allow
const lines = settings.minLines === 0 && settings.maxLines >= 1 ? 1 : settings.minLines;
let baseNode =
/**
* @type {import('@typescript-eslint/types').TSESTree.Node|import('eslint').Rule.Node}
*/ (
getReducedASTNode(node, sourceCode)
);
const decorator = getDecorator(
/** @type {import('eslint').Rule.Node} */
(baseNode)
);
if (decorator) {
baseNode = /** @type {import('@typescript-eslint/types').TSESTree.Decorator} */ (
decorator
);
}
const indent = getIndent({
text: sourceCode.getText(
/** @type {import('eslint').Rule.Node} */ (baseNode),
/** @type {import('eslint').AST.SourceLocation} */
(
/** @type {import('eslint').Rule.Node} */ (baseNode).loc
).start.column,
),
});
const {
inlineCommentBlock,
} =
/**
* @type {{
* context: string,
* inlineCommentBlock: boolean,
* minLineCount: import('../iterateJsdoc.js').Integer
* }[]}
*/ (ctxts).find((contxt) => {
if (typeof contxt === 'string') {
return false;
}
const {
context: ctxt,
} = contxt;
return ctxt === node.type;
}) || {};
return addComment(inlineCommentBlock, comment, indent, lines, fixer);
};
};
/**
* @param {import('eslint').AST.Token | import('estree').Comment | {
* type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
* range: [number, number],
* value: string
* }} comment
* @param {import('eslint').Rule.Node} node
* @param {AddComment} addComment
* @param {import('../iterateJsdoc.js').Context[]} ctxts
*/
const reportings = (comment, node, addComment, ctxts) => {
const fixer = getFixer(node, comment, addComment, ctxts);
if (comment.type === 'Block') {
if (lineOrBlockStyle === 'line') {
return;
}
report('blockCommentsJsdocStyle', comment, node, fixer);
return;
}
if (comment.type === 'Line') {
if (lineOrBlockStyle === 'block') {
return;
}
report('lineCommentsJsdocStyle', comment, node, fixer);
}
};
/**
* @type {import('../iterateJsdoc.js').CheckJsdoc}
*/
const checkNonJsdoc = (_info, _handler, node) => {
const comment = getNonJsdocComment(sourceCode, node, settings);
if (
!comment ||
/** @type {string[]} */
(allowedPrefixes).some((prefix) => {
return comment.value.trimStart().startsWith(prefix);
})
) {
return;
}
reportingNonJsdoc = true;
/** @type {AddComment} */
const addComment = (inlineCommentBlock, comment, indent, lines, fixer) => {
const insertion = (
inlineCommentBlock || enforceJsdocLineStyle === 'single'
? `/** ${comment.value.trim()} `
: `/**\n${indent}*${comment.value.trimEnd()}\n${indent}`
) +
`*/${'\n'.repeat((lines || 1) - 1)}`;
return fixer.replaceText(
/** @type {import('eslint').AST.Token} */
(comment),
insertion,
);
};
reportings(comment, node, addComment, contexts);
};
/**
* @param {import('eslint').Rule.Node} node
* @param {import('../iterateJsdoc.js').Context[]} ctxts
*/
const checkNonJsdocAfter = (node, ctxts) => {
const comment = getFollowingComment(sourceCode, node);
if (
!comment ||
comment.value.startsWith('*') ||
/** @type {string[]} */
(allowedPrefixes).some((prefix) => {
return comment.value.trimStart().startsWith(prefix);
})
) {
return;
}
/** @type {AddComment} */
const addComment = (inlineCommentBlock, comment, indent, lines, fixer) => {
const insertion = (
inlineCommentBlock || enforceJsdocLineStyle === 'single'
? `/** ${comment.value.trim()} `
: `/**\n${indent}*${comment.value.trimEnd()}\n${indent}`
) +
`*/${'\n'.repeat((lines || 1) - 1)}${lines ? `\n${indent.slice(1)}` : ' '}`;
return [fixer.remove(
/** @type {import('eslint').AST.Token} */
(comment)
), fixer.insertTextBefore(
node.type === 'VariableDeclarator' ? node.parent : node,
insertion,
)];
};
reportings(comment, node, addComment, ctxts);
};
// Todo: add contexts to check after (and handle if want both before and after)
return {
...getContextObject(
enforcedContexts(context, true, settings),
checkNonJsdoc,
),
...getContextObject(
contextsAfter,
(_info, _handler, node) => {
checkNonJsdocAfter(node, contextsAfter);
},
),
...getContextObject(
contextsBeforeAndAfter,
(_info, _handler, node) => {
checkNonJsdoc({}, null, node);
if (!reportingNonJsdoc) {
checkNonJsdocAfter(node, contextsBeforeAndAfter);
}
}
)
};
},
meta: {
fixable: 'code',
messages: {
blockCommentsJsdocStyle: 'Block comments should be JSDoc-style.',
lineCommentsJsdocStyle: 'Line comments should be JSDoc-style.',
},
docs: {
description: 'Converts non-JSDoc comments preceding or following nodes into JSDoc ones',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/convert-to-jsdoc-comments.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
allowedPrefixes: {
type: 'array',
items: {
type: 'string'
}
},
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
context: {
type: 'string',
},
inlineCommentBlock: {
type: 'boolean',
},
},
type: 'object',
},
],
},
type: 'array',
},
contextsAfter: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
context: {
type: 'string',
},
inlineCommentBlock: {
type: 'boolean',
},
},
type: 'object',
},
],
},
type: 'array',
},
contextsBeforeAndAfter: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
context: {
type: 'string',
},
inlineCommentBlock: {
type: 'boolean',
},
},
type: 'object',
},
],
},
type: 'array',
},
enableFixer: {
type: 'boolean'
},
enforceJsdocLineStyle: {
type: 'string',
enum: ['multi', 'single']
},
lineOrBlockStyle: {
type: 'string',
enum: ['block', 'line', 'both']
},
},
type: 'object',
},
],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,88 @@
import iterateJsdoc from '../iterateJsdoc.js';
const defaultEmptyTags = new Set([
'abstract', 'async', 'generator', 'global', 'hideconstructor',
'ignore', 'inner', 'instance', 'override', 'readonly',
// jsdoc doesn't use this form in its docs, but allow for compatibility with
// TypeScript which allows and Closure which requires
'inheritDoc',
// jsdoc doesn't use but allow for TypeScript
'internal',
'overload',
]);
const emptyIfNotClosure = new Set([
'package', 'private', 'protected', 'public', 'static',
// Closure doesn't allow with this casing
'inheritdoc',
]);
const emptyIfClosure = new Set([
'interface',
]);
export default iterateJsdoc(({
settings,
jsdoc,
utils,
}) => {
const emptyTags = utils.filterTags(({
tag: tagName,
}) => {
return defaultEmptyTags.has(tagName) ||
utils.hasOptionTag(tagName) && jsdoc.tags.some(({
tag,
}) => {
return tag === tagName;
}) ||
settings.mode === 'closure' && emptyIfClosure.has(tagName) ||
settings.mode !== 'closure' && emptyIfNotClosure.has(tagName);
});
for (const tag of emptyTags) {
const content = tag.name || tag.description || tag.type;
if (content.trim()) {
const fix = () => {
// By time of call in fixer, `tag` will have `line` added
utils.setTag(
/**
* @type {import('comment-parser').Spec & {
* line: import('../iterateJsdoc.js').Integer
* }}
*/ (tag),
);
};
utils.reportJSDoc(`@${tag.tag} should be empty.`, tag, fix, true);
}
}
}, {
checkInternal: true,
checkPrivate: true,
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Expects specific tags to be empty of any content.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/empty-tags.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
tags: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,64 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
report,
utils,
}) => {
const iteratingFunction = utils.isIteratingFunction();
if (iteratingFunction) {
if (utils.hasATag([
'class',
'constructor',
]) ||
utils.isConstructor()
) {
return;
}
} else if (!utils.isVirtualFunction()) {
return;
}
utils.forEachPreferredTag('implements', (tag) => {
report('@implements used on a non-constructor function', null, tag);
});
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Reports an issue with any non-constructor function using `@implements`.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/implements-on-classes.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,131 @@
import iterateJsdoc from '../iterateJsdoc.js';
import {
parse,
traverse,
tryParse,
} from '@es-joy/jsdoccomment';
import {
readFileSync,
} from 'fs';
import {isBuiltin as isBuiltinModule} from 'node:module';
import {
join,
} from 'path';
/**
* @type {Set<string>|null}
*/
let deps;
const setDeps = function () {
try {
const pkg = JSON.parse(
// @ts-expect-error It's ok
readFileSync(join(process.cwd(), './package.json')),
);
deps = new Set([
...(pkg.dependencies ?
/* c8 ignore next 2 */
Object.keys(pkg.dependencies) :
[]),
...(pkg.devDependencies ?
/* c8 ignore next 2 */
Object.keys(pkg.devDependencies) :
[]),
]);
/* c8 ignore next -- our package.json exists */
} catch (error) {
/* c8 ignore next -- our package.json exists */
deps = null;
/* c8 ignore next 4 -- our package.json exists */
/* eslint-disable no-console -- Inform user */
console.log(error);
/* eslint-enable no-console -- Inform user */
}
};
const moduleCheck = new Map();
export default iterateJsdoc(({
jsdoc,
settings,
utils,
}) => {
if (deps === undefined) {
setDeps();
}
/* c8 ignore next 3 -- our package.json exists */
if (deps === null) {
return;
}
const {
mode,
} = settings;
for (const tag of jsdoc.tags) {
let typeAst;
try {
typeAst = mode === 'permissive' ? tryParse(tag.type) : parse(tag.type, mode);
} catch {
continue;
}
// eslint-disable-next-line no-loop-func -- Safe
traverse(typeAst, (nde) => {
/* c8 ignore next 3 -- TS guard */
if (deps === null) {
return;
}
if (nde.type === 'JsdocTypeImport') {
let mod = nde.element.value.replace(
/^(@[^/]+\/[^/]+|[^/]+).*$/u, '$1',
);
if ((/^[./]/u).test(mod)) {
return;
}
if (isBuiltinModule(mod)) {
// mod = '@types/node';
// moduleCheck.set(mod, !deps.has(mod));
return;
} else if (!moduleCheck.has(mod)) {
let pkg;
try {
pkg = JSON.parse(
// @ts-expect-error It's ok
readFileSync(join(process.cwd(), 'node_modules', mod, './package.json')),
);
} catch {
// Ignore
}
if (!pkg || (!pkg.types && !pkg.typings)) {
mod = `@types/${mod}`;
}
moduleCheck.set(mod, !deps.has(mod));
}
if (moduleCheck.get(mod)) {
utils.reportJSDoc(
'import points to package which is not found in dependencies',
tag,
);
}
}
});
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports if JSDoc `import()` statements point to a package which is not listed in `dependencies` or `devDependencies`',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/imports-as-dependencies.md#repos-sticky-header',
},
type: 'suggestion',
},
});

View File

@@ -0,0 +1,189 @@
import iterateJsdoc from '../iterateJsdoc.js';
import {
areDocsInformative,
} from 'are-docs-informative';
const defaultAliases = {
a: [
'an', 'our',
],
};
const defaultUselessWords = [
'a', 'an', 'i', 'in', 'of', 's', 'the',
];
/* eslint-disable complexity -- Temporary */
/**
* @param {import('eslint').Rule.Node|import('@typescript-eslint/types').TSESTree.Node|null|undefined} node
* @returns {string[]}
*/
const getNamesFromNode = (node) => {
switch (node?.type) {
case 'AccessorProperty':
case 'MethodDefinition':
case 'PropertyDefinition':
case 'TSAbstractAccessorProperty':
case 'TSAbstractMethodDefinition':
case 'TSAbstractPropertyDefinition':
return [
...getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.Node} */ (
node.parent
).parent,
),
...getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.Node} */
(node.key),
),
];
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
return getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.ExportNamedDeclaration} */
(node).declaration
);
case 'ClassDeclaration':
case 'ClassExpression':
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'TSModuleDeclaration':
case 'TSMethodSignature':
case 'TSDeclareFunction':
case 'TSEnumDeclaration':
case 'TSEnumMember':
case 'TSInterfaceDeclaration':
case 'TSTypeAliasDeclaration':
return getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.ClassDeclaration} */
(node).id,
);
case 'Identifier':
return [
node.name,
];
case 'Property':
return getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.Node} */
(node.key),
);
case 'VariableDeclaration':
return getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.Node} */
(node.declarations[0]),
);
case 'VariableDeclarator':
return [
...getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.Node} */
(node.id),
),
...getNamesFromNode(
/** @type {import('@typescript-eslint/types').TSESTree.Node} */
(node.init),
),
].filter(Boolean);
default:
return [];
}
};
/* eslint-enable complexity -- Temporary */
export default iterateJsdoc(({
context,
jsdoc,
node,
report,
utils,
}) => {
const /** @type {{aliases: {[key: string]: string[]}, excludedTags: string[], uselessWords: string[]}} */ {
aliases = defaultAliases,
excludedTags = [],
uselessWords = defaultUselessWords,
} = context.options[0] || {};
const nodeNames = getNamesFromNode(node);
/**
* @param {string} text
* @param {string} extraName
* @returns {boolean}
*/
const descriptionIsRedundant = (text, extraName = '') => {
const textTrimmed = text.trim();
return Boolean(textTrimmed) && !areDocsInformative(textTrimmed, [
extraName, nodeNames,
].filter(Boolean).join(' '), {
aliases,
uselessWords,
});
};
const {
description,
lastDescriptionLine,
} = utils.getDescription();
let descriptionReported = false;
for (const tag of jsdoc.tags) {
if (excludedTags.includes(tag.tag)) {
continue;
}
if (descriptionIsRedundant(tag.description, tag.name)) {
utils.reportJSDoc(
'This tag description only repeats the name it describes.',
tag,
);
}
descriptionReported ||= tag.description === description &&
/** @type {import('comment-parser').Spec & {line: import('../iterateJsdoc.js').Integer}} */
(tag).line === lastDescriptionLine;
}
if (!descriptionReported && descriptionIsRedundant(description)) {
report('This description only repeats the name it describes.');
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description:
'This rule reports doc comments that only restate their attached name.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/informative-docs.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
aliases: {
patternProperties: {
'.*': {
items: {
type: 'string',
},
type: 'array',
},
},
},
excludedTags: {
items: {
type: 'string',
},
type: 'array',
},
uselessWords: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,286 @@
import iterateJsdoc from '../iterateJsdoc.js';
// If supporting Node >= 10, we could loosen the default to this for the
// initial letter: \\p{Upper}
const matchDescriptionDefault = '^\n?([A-Z`\\d_][\\s\\S]*[.?!`]\\s*)?$';
/**
* @param {string} value
* @param {string} userDefault
* @returns {string}
*/
const stringOrDefault = (value, userDefault) => {
return typeof value === 'string' ?
value :
userDefault || matchDescriptionDefault;
};
export default iterateJsdoc(({
jsdoc,
report,
context,
utils,
}) => {
const {
mainDescription,
matchDescription,
message,
nonemptyTags = true,
tags = {},
} = context.options[0] || {};
/**
* @param {string} desc
* @param {import('comment-parser').Spec} [tag]
* @returns {void}
*/
const validateDescription = (desc, tag) => {
let mainDescriptionMatch = mainDescription;
let errorMessage = message;
if (typeof mainDescription === 'object') {
mainDescriptionMatch = mainDescription.match;
errorMessage = mainDescription.message;
}
if (mainDescriptionMatch === false && (
!tag || !Object.prototype.hasOwnProperty.call(tags, tag.tag))
) {
return;
}
let tagValue = mainDescriptionMatch;
if (tag) {
const tagName = tag.tag;
if (typeof tags[tagName] === 'object') {
tagValue = tags[tagName].match;
errorMessage = tags[tagName].message;
} else {
tagValue = tags[tagName];
}
}
const regex = utils.getRegexFromString(
stringOrDefault(tagValue, matchDescription),
);
if (!regex.test(desc)) {
report(
errorMessage || 'JSDoc description does not satisfy the regex pattern.',
null,
tag || {
// Add one as description would typically be into block
line: jsdoc.source[0].number + 1,
},
);
}
};
const {
description,
} = utils.getDescription();
if (description) {
validateDescription(description);
}
/**
* @param {string} tagName
* @returns {boolean}
*/
const hasNoTag = (tagName) => {
return !tags[tagName];
};
for (const tag of [
'description',
'summary',
'file',
'classdesc',
]) {
utils.forEachPreferredTag(tag, (matchingJsdocTag, targetTagName) => {
const desc = (matchingJsdocTag.name + ' ' + utils.getTagDescription(matchingJsdocTag)).trim();
if (hasNoTag(targetTagName)) {
validateDescription(desc, matchingJsdocTag);
}
}, true);
}
if (nonemptyTags) {
for (const tag of [
'copyright',
'example',
'see',
'todo',
]) {
utils.forEachPreferredTag(tag, (matchingJsdocTag, targetTagName) => {
const desc = (matchingJsdocTag.name + ' ' + utils.getTagDescription(matchingJsdocTag)).trim();
if (hasNoTag(targetTagName) && !(/.+/u).test(desc)) {
report(
'JSDoc description must not be empty.',
null,
matchingJsdocTag,
);
}
});
}
}
if (!Object.keys(tags).length) {
return;
}
/**
* @param {string} tagName
* @returns {boolean}
*/
const hasOptionTag = (tagName) => {
return Boolean(tags[tagName]);
};
const whitelistedTags = utils.filterTags(({
tag: tagName,
}) => {
return hasOptionTag(tagName);
});
const {
tagsWithNames,
tagsWithoutNames,
} = utils.getTagsByType(whitelistedTags);
tagsWithNames.some((tag) => {
const desc = /** @type {string} */ (
utils.getTagDescription(tag)
).replace(/^[- ]*/u, '')
.trim();
return validateDescription(desc, tag);
});
tagsWithoutNames.some((tag) => {
const desc = (tag.name + ' ' + utils.getTagDescription(tag)).trim();
return validateDescription(desc, tag);
});
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Enforces a regular expression pattern on descriptions.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/match-description.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
mainDescription: {
oneOf: [
{
format: 'regex',
type: 'string',
},
{
type: 'boolean',
},
{
additionalProperties: false,
properties: {
match: {
oneOf: [
{
format: 'regex',
type: 'string',
},
{
type: 'boolean',
},
],
},
message: {
type: 'string',
},
},
type: 'object',
},
],
},
matchDescription: {
format: 'regex',
type: 'string',
},
message: {
type: 'string',
},
nonemptyTags: {
type: 'boolean',
},
tags: {
patternProperties: {
'.*': {
oneOf: [
{
format: 'regex',
type: 'string',
},
{
enum: [
true,
],
type: 'boolean',
},
{
additionalProperties: false,
properties: {
match: {
oneOf: [
{
format: 'regex',
type: 'string',
},
{
enum: [
true,
],
type: 'boolean',
},
],
},
message: {
type: 'string',
},
},
type: 'object',
},
],
},
},
type: 'object',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

147
node_modules/eslint-plugin-jsdoc/src/rules/matchName.js generated vendored Normal file
View File

@@ -0,0 +1,147 @@
import iterateJsdoc from '../iterateJsdoc.js';
// eslint-disable-next-line complexity
export default iterateJsdoc(({
context,
jsdoc,
report,
info: {
lastIndex,
},
utils,
}) => {
const {
match,
} = context.options[0] || {};
if (!match) {
report('Rule `no-restricted-syntax` is missing a `match` option.');
return;
}
const {
allowName,
disallowName,
replacement,
tags = [
'*',
],
} = match[/** @type {import('../iterateJsdoc.js').Integer} */ (lastIndex)];
const allowNameRegex = allowName && utils.getRegexFromString(allowName);
const disallowNameRegex = disallowName && utils.getRegexFromString(disallowName);
let applicableTags = jsdoc.tags;
if (!tags.includes('*')) {
applicableTags = utils.getPresentTags(tags);
}
let reported = false;
for (const tag of applicableTags) {
const allowed = !allowNameRegex || allowNameRegex.test(tag.name);
const disallowed = disallowNameRegex && disallowNameRegex.test(tag.name);
const hasRegex = allowNameRegex || disallowNameRegex;
if (hasRegex && allowed && !disallowed) {
continue;
}
if (!hasRegex && reported) {
continue;
}
const fixer = () => {
for (const src of tag.source) {
if (src.tokens.name) {
src.tokens.name = src.tokens.name.replace(
disallowNameRegex, replacement,
);
break;
}
}
};
let {
message,
} = match[/** @type {import('../iterateJsdoc.js').Integer} */ (lastIndex)];
if (!message) {
if (hasRegex) {
message = disallowed ?
`Only allowing names not matching \`${disallowNameRegex}\` but found "${tag.name}".` :
`Only allowing names matching \`${allowNameRegex}\` but found "${tag.name}".`;
} else {
message = `Prohibited context for "${tag.name}".`;
}
}
utils.reportJSDoc(
message,
hasRegex ? tag : null,
// We could match up
disallowNameRegex && replacement !== undefined ?
fixer :
null,
false,
{
// Could also supply `context`, `comment`, `tags`
allowName,
disallowName,
name: tag.name,
},
);
if (!hasRegex) {
reported = true;
}
}
}, {
matchContext: true,
meta: {
docs: {
description: 'Reports the name portion of a JSDoc tag if matching or not matching a given regular expression.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/match-name.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
match: {
additionalProperties: false,
items: {
properties: {
allowName: {
type: 'string',
},
comment: {
type: 'string',
},
context: {
type: 'string',
},
disallowName: {
type: 'string',
},
message: {
type: 'string',
},
tags: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
type: 'array',
},
},
required: [
'match',
],
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,333 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
jsdoc,
utils,
}) => {
const {
allowMultipleTags = true,
noFinalLineText = true,
noZeroLineText = true,
noSingleLineBlocks = false,
singleLineTags = [
'lends', 'type',
],
noMultilineBlocks = false,
minimumLengthForMultiline = Number.POSITIVE_INFINITY,
multilineTags = [
'*',
],
} = context.options[0] || {};
const {
source: [
{
tokens,
},
],
} = jsdoc;
const {
description,
tag,
} = tokens;
const sourceLength = jsdoc.source.length;
/**
* @param {string} tagName
* @returns {boolean}
*/
const isInvalidSingleLine = (tagName) => {
return noSingleLineBlocks &&
(!tagName ||
!singleLineTags.includes(tagName) && !singleLineTags.includes('*'));
};
if (sourceLength === 1) {
if (!isInvalidSingleLine(tag.slice(1))) {
return;
}
const fixer = () => {
utils.makeMultiline();
};
utils.reportJSDoc(
'Single line blocks are not permitted by your configuration.',
null,
fixer,
true,
);
return;
}
const lineChecks = () => {
if (
noZeroLineText &&
(tag || description)
) {
const fixer = () => {
const line = {
...tokens,
};
utils.emptyTokens(tokens);
const {
tokens: {
delimiter,
start,
},
} = jsdoc.source[1];
utils.addLine(1, {
...line,
delimiter,
start,
});
};
utils.reportJSDoc(
'Should have no text on the "0th" line (after the `/**`).',
null,
fixer,
);
return;
}
const finalLine = jsdoc.source[jsdoc.source.length - 1];
const finalLineTokens = finalLine.tokens;
if (
noFinalLineText &&
finalLineTokens.description.trim()
) {
const fixer = () => {
const line = {
...finalLineTokens,
};
line.description = line.description.trimEnd();
const {
delimiter,
} = line;
for (const prop of [
'delimiter',
'postDelimiter',
'tag',
'type',
'lineEnd',
'postType',
'postTag',
'name',
'postName',
'description',
]) {
finalLineTokens[
/**
* @type {"delimiter"|"postDelimiter"|"tag"|"type"|
* "lineEnd"|"postType"|"postTag"|"name"|
* "postName"|"description"}
*/ (
prop
)
] = '';
}
utils.addLine(jsdoc.source.length - 1, {
...line,
delimiter,
end: '',
});
};
utils.reportJSDoc(
'Should have no text on the final line (before the `*/`).',
null,
fixer,
);
}
};
if (noMultilineBlocks) {
if (
jsdoc.tags.length &&
(multilineTags.includes('*') || utils.hasATag(multilineTags))
) {
lineChecks();
return;
}
if (jsdoc.description.length >= minimumLengthForMultiline) {
lineChecks();
return;
}
if (
noSingleLineBlocks &&
(!jsdoc.tags.length ||
!utils.filterTags(({
tag: tg,
}) => {
return !isInvalidSingleLine(tg);
}).length)
) {
utils.reportJSDoc(
'Multiline jsdoc blocks are prohibited by ' +
'your configuration but fixing would result in a single ' +
'line block which you have prohibited with `noSingleLineBlocks`.',
);
return;
}
if (jsdoc.tags.length > 1) {
if (!allowMultipleTags) {
utils.reportJSDoc(
'Multiline jsdoc blocks are prohibited by ' +
'your configuration but the block has multiple tags.',
);
return;
}
} else if (jsdoc.tags.length === 1 && jsdoc.description.trim()) {
if (!allowMultipleTags) {
utils.reportJSDoc(
'Multiline jsdoc blocks are prohibited by ' +
'your configuration but the block has a description with a tag.',
);
return;
}
} else {
const fixer = () => {
jsdoc.source = [
{
number: 1,
source: '',
tokens: jsdoc.source.reduce((obj, {
tokens: {
description: desc,
tag: tg,
type: typ,
name: nme,
lineEnd,
postType,
postName,
postTag,
},
}) => {
if (typ) {
obj.type = typ;
}
if (tg && typ && nme) {
obj.postType = postType;
}
if (nme) {
obj.name += nme;
}
if (nme && desc) {
obj.postName = postName;
}
obj.description += desc;
const nameOrDescription = obj.description || obj.name;
if (
nameOrDescription && nameOrDescription.slice(-1) !== ' '
) {
obj.description += ' ';
}
obj.lineEnd = lineEnd;
// Already filtered for multiple tags
obj.tag += tg;
if (tg) {
obj.postTag = postTag || ' ';
}
return obj;
}, utils.seedTokens({
delimiter: '/**',
end: '*/',
postDelimiter: ' ',
})),
},
];
};
utils.reportJSDoc(
'Multiline jsdoc blocks are prohibited by ' +
'your configuration.',
null,
fixer,
);
return;
}
}
lineChecks();
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Controls how and whether jsdoc blocks can be expressed as single or multiple line blocks.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/multiline-blocks.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
allowMultipleTags: {
type: 'boolean',
},
minimumLengthForMultiline: {
type: 'integer',
},
multilineTags: {
anyOf: [
{
enum: [
'*',
],
type: 'string',
}, {
items: {
type: 'string',
},
type: 'array',
},
],
},
noFinalLineText: {
type: 'boolean',
},
noMultilineBlocks: {
type: 'boolean',
},
noSingleLineBlocks: {
type: 'boolean',
},
noZeroLineText: {
type: 'boolean',
},
singleLineTags: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,109 @@
import iterateJsdoc from '../iterateJsdoc.js';
import {
parse as commentParser,
} from 'comment-parser';
// Neither a single nor 3+ asterisks are valid jsdoc per
// https://jsdoc.app/about-getting-started.html#adding-documentation-comments-to-your-code
const commentRegexp = /^\/\*(?!\*)/u;
const extraAsteriskCommentRegexp = /^\/\*{3,}/u;
export default iterateJsdoc(({
context,
sourceCode,
allComments,
makeReport,
}) => {
const [
{
ignore = [
'ts-check',
'ts-expect-error',
'ts-ignore',
'ts-nocheck',
],
preventAllMultiAsteriskBlocks = false,
} = {},
] = context.options;
let extraAsterisks = false;
const nonJsdocNodes = /** @type {import('estree').Node[]} */ (
allComments
).filter((comment) => {
const commentText = sourceCode.getText(comment);
let sliceIndex = 2;
if (!commentRegexp.test(commentText)) {
const multiline = extraAsteriskCommentRegexp.exec(commentText)?.[0];
if (!multiline) {
return false;
}
sliceIndex = multiline.length;
extraAsterisks = true;
if (preventAllMultiAsteriskBlocks) {
return true;
}
}
const tags = (commentParser(
`${commentText.slice(0, 2)}*${commentText.slice(sliceIndex)}`,
)[0] || {}).tags ?? [];
return tags.length && !tags.some(({
tag,
}) => {
return ignore.includes(tag);
});
});
if (!nonJsdocNodes.length) {
return;
}
for (const node of nonJsdocNodes) {
const report = /** @type {import('../iterateJsdoc.js').MakeReport} */ (
makeReport
)(context, node);
// eslint-disable-next-line no-loop-func
const fix = /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
const text = sourceCode.getText(node);
return fixer.replaceText(
node,
extraAsterisks ?
text.replace(extraAsteriskCommentRegexp, '/**') :
text.replace('/*', '/**'),
);
};
report('Expected JSDoc-like comment to begin with two asterisks.', fix);
}
}, {
checkFile: true,
meta: {
docs: {
description: 'This rule checks for multi-line-style comments which fail to meet the criteria of a jsdoc block.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-bad-blocks.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
ignore: {
items: {
type: 'string',
},
type: 'array',
},
preventAllMultiAsteriskBlocks: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'layout',
},
});

View File

@@ -0,0 +1,69 @@
import iterateJsdoc from '../iterateJsdoc.js';
const anyWhitespaceLines = /^\s*$/u;
const atLeastTwoLinesWhitespace = /^[ \t]*\n[ \t]*\n\s*$/u;
export default iterateJsdoc(({
jsdoc,
utils,
}) => {
const {
description,
descriptions,
lastDescriptionLine,
} = utils.getDescription();
const regex = jsdoc.tags.length ?
anyWhitespaceLines :
atLeastTwoLinesWhitespace;
if (descriptions.length && regex.test(description)) {
if (jsdoc.tags.length) {
utils.reportJSDoc(
'There should be no blank lines in block descriptions followed by tags.',
{
line: lastDescriptionLine,
},
() => {
utils.setBlockDescription(() => {
// Remove all lines
return [];
});
},
);
} else {
utils.reportJSDoc(
'There should be no extra blank lines in block descriptions not followed by tags.',
{
line: lastDescriptionLine,
},
() => {
utils.setBlockDescription((info, seedTokens) => {
return [
// Keep the starting line
{
number: 0,
source: '',
tokens: seedTokens({
...info,
description: '',
}),
},
];
});
},
);
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Detects and removes extra lines of a blank block description',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-blank-block-descriptions.md#repos-sticky-header',
},
fixable: 'whitespace',
schema: [],
type: 'layout',
},
});

View File

@@ -0,0 +1,53 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
jsdoc,
utils,
}) => {
if (jsdoc.tags.length) {
return;
}
const {
description,
lastDescriptionLine,
} = utils.getDescription();
if (description.trim()) {
return;
}
const {
enableFixer,
} = context.options[0] || {};
utils.reportJSDoc(
'No empty blocks',
{
line: lastDescriptionLine,
},
enableFixer ? () => {
jsdoc.source.splice(0, jsdoc.source.length);
} : null,
);
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Removes empty blocks with nothing but possibly line breaks',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-blank-blocks.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
enableFixer: {
type: 'boolean',
},
},
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,85 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
utils,
}) => {
const {
noOptionalParamNames,
} = context.options[0] || {};
const paramTags = utils.getPresentTags([
'param', 'arg', 'argument',
]);
for (const tag of paramTags) {
if (noOptionalParamNames && tag.optional) {
utils.reportJSDoc(`Optional param names are not permitted on @${tag.tag}.`, tag, () => {
utils.changeTag(tag, {
name: tag.name.replace(/([^=]*)(=.+)?/u, '$1'),
});
});
} else if (tag.default) {
utils.reportJSDoc(`Defaults are not permitted on @${tag.tag}.`, tag, () => {
utils.changeTag(tag, {
name: tag.name.replace(/([^=]*)(=.+)?/u, '[$1]'),
});
});
}
}
const defaultTags = utils.getPresentTags([
'default', 'defaultvalue',
]);
for (const tag of defaultTags) {
if (tag.description.trim()) {
utils.reportJSDoc(`Default values are not permitted on @${tag.tag}.`, tag, () => {
utils.changeTag(tag, {
description: '',
postTag: '',
});
});
}
}
}, {
contextDefaults: true,
meta: {
docs: {
description: 'This rule reports defaults being used on the relevant portion of `@param` or `@default`.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-defaults.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
noOptionalParamNames: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,195 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @typedef {{
* comment: string,
* context: string,
* message: string,
* minimum: import('../iterateJsdoc.js').Integer
* }} ContextObject
*/
/**
* @typedef {string|ContextObject} Context
*/
/**
* @param {import('../iterateJsdoc.js').StateObject} state
* @returns {void}
*/
const setDefaults = (state) => {
if (!state.selectorMap) {
state.selectorMap = {};
}
};
/**
* @param {import('../iterateJsdoc.js').StateObject} state
* @param {string} selector
* @param {string} comment
* @returns {void}
*/
const incrementSelector = (state, selector, comment) => {
if (!state.selectorMap[selector]) {
state.selectorMap[selector] = {};
}
if (!state.selectorMap[selector][comment]) {
state.selectorMap[selector][comment] = 0;
}
state.selectorMap[selector][comment]++;
};
export default iterateJsdoc(({
context,
info: {
comment,
},
state,
utils,
}) => {
if (!context.options[0]) {
// Handle error later
return;
}
/**
* @type {Context[]}
*/
const contexts = context.options[0].contexts;
const {
contextStr,
} = utils.findContext(contexts, comment);
setDefaults(state);
incrementSelector(state, contextStr, String(comment));
}, {
contextSelected: true,
exit ({
context,
settings,
state,
}) {
if (!context.options.length && !settings.contexts) {
context.report({
loc: {
end: {
column: 1,
line: 1,
},
start: {
column: 1,
line: 1,
},
},
message: 'Rule `no-missing-syntax` is missing a `contexts` option.',
});
return;
}
setDefaults(state);
/**
* @type {Context[]}
*/
const contexts = (context.options[0] ?? {}).contexts ?? settings?.contexts;
// Report when MISSING
contexts.some((cntxt) => {
const contextStr = typeof cntxt === 'object' ? cntxt.context ?? 'any' : cntxt;
const comment = typeof cntxt === 'string' ? '' : cntxt?.comment;
const contextKey = contextStr === 'any' ? 'undefined' : contextStr;
if (
(!state.selectorMap[contextKey] ||
!state.selectorMap[contextKey][comment] ||
state.selectorMap[contextKey][comment] < (
// @ts-expect-error comment would need an object, not string
cntxt?.minimum ?? 1
)) &&
(contextStr !== 'any' || Object.values(state.selectorMap).every((cmmnt) => {
return !cmmnt[comment] || cmmnt[comment] < (
// @ts-expect-error comment would need an object, not string
cntxt?.minimum ?? 1
);
}))
) {
const message = typeof cntxt === 'string' ?
'Syntax is required: {{context}}' :
cntxt?.message ?? ('Syntax is required: {{context}}' +
(comment ? ' with {{comment}}' : ''));
context.report({
data: {
comment,
context: contextStr,
},
loc: {
end: {
column: 1,
line: 1,
},
start: {
column: 1,
line: 1,
},
},
message,
});
return true;
}
return false;
});
},
matchContext: true,
meta: {
docs: {
description: 'Reports when certain comment structures are always expected.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-missing-syntax.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
message: {
type: 'string',
},
minimum: {
type: 'integer',
},
},
type: 'object',
},
],
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,134 @@
import iterateJsdoc from '../iterateJsdoc.js';
const middleAsterisksBlockWS = /^([\t ]|\*(?!\*))+/u;
const middleAsterisksNoBlockWS = /^\*+/u;
const endAsterisksSingleLineBlockWS = /\*((?:\*|(?: |\t))*)\*$/u;
const endAsterisksMultipleLineBlockWS = /((?:\*|(?: |\t))*)\*$/u;
const endAsterisksSingleLineNoBlockWS = /\*(\**)\*$/u;
const endAsterisksMultipleLineNoBlockWS = /(\**)\*$/u;
export default iterateJsdoc(({
context,
jsdoc,
utils,
}) => {
const {
allowWhitespace = false,
preventAtEnd = true,
preventAtMiddleLines = true,
} = context.options[0] || {};
const middleAsterisks = allowWhitespace ? middleAsterisksNoBlockWS : middleAsterisksBlockWS;
// eslint-disable-next-line complexity -- Todo
jsdoc.source.some(({
tokens,
number,
}) => {
const {
delimiter,
tag,
name,
type,
description,
end,
postDelimiter,
} = tokens;
if (
preventAtMiddleLines &&
!end && !tag && !type && !name &&
(
!allowWhitespace && middleAsterisks.test(description) ||
allowWhitespace && middleAsterisks.test(postDelimiter + description)
)
) {
// console.log('description', JSON.stringify(description));
const fix = () => {
tokens.description = description.replace(middleAsterisks, '');
};
utils.reportJSDoc(
'Should be no multiple asterisks on middle lines.',
{
line: number,
},
fix,
true,
);
return true;
}
if (!preventAtEnd || !end) {
return false;
}
const isSingleLineBlock = delimiter === '/**';
const delim = isSingleLineBlock ? '*' : delimiter;
const endAsterisks = allowWhitespace ?
(isSingleLineBlock ? endAsterisksSingleLineNoBlockWS : endAsterisksMultipleLineNoBlockWS) :
(isSingleLineBlock ? endAsterisksSingleLineBlockWS : endAsterisksMultipleLineBlockWS);
const endingAsterisksAndSpaces = (
allowWhitespace ? postDelimiter + description + delim : description + delim
).match(
endAsterisks,
);
if (
!endingAsterisksAndSpaces ||
!isSingleLineBlock && endingAsterisksAndSpaces[1] && !endingAsterisksAndSpaces[1].trim()
) {
return false;
}
const endFix = () => {
if (!isSingleLineBlock) {
tokens.delimiter = '';
}
tokens.description = (description + delim).replace(endAsterisks, '');
};
utils.reportJSDoc(
'Should be no multiple asterisks on end lines.',
{
line: number,
},
endFix,
true,
);
return true;
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: '',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-multi-asterisks.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
allowWhitespace: {
type: 'boolean',
},
preventAtEnd: {
type: 'boolean',
},
preventAtMiddleLines: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,91 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
info: {
comment,
},
report,
utils,
}) => {
if (!context.options.length) {
report('Rule `no-restricted-syntax` is missing a `contexts` option.');
return;
}
const {
contexts,
} = context.options[0];
const {
foundContext,
contextStr,
} = utils.findContext(contexts, comment);
// We are not on the *particular* matching context/comment, so don't assume
// we need reporting
if (!foundContext) {
return;
}
const message = /** @type {import('../iterateJsdoc.js').ContextObject} */ (
foundContext
)?.message ??
'Syntax is restricted: {{context}}' +
(comment ? ' with {{comment}}' : '');
report(message, null, null, comment ? {
comment,
context: contextStr,
} : {
context: contextStr,
});
}, {
contextSelected: true,
meta: {
docs: {
description: 'Reports when certain comment structures are present.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-restricted-syntax.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
message: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
},
required: [
'contexts',
],
type: 'object',
},
],
type: 'suggestion',
},
nonGlobalSettings: true,
});

77
node_modules/eslint-plugin-jsdoc/src/rules/noTypes.js generated vendored Normal file
View File

@@ -0,0 +1,77 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {import('comment-parser').Line} line
*/
const removeType = ({
tokens,
}) => {
tokens.postTag = '';
tokens.type = '';
};
export default iterateJsdoc(({
utils,
}) => {
if (!utils.isIteratingFunction() && !utils.isVirtualFunction()) {
return;
}
const tags = utils.getPresentTags([
'param', 'arg', 'argument', 'returns', 'return',
]);
for (const tag of tags) {
if (tag.type) {
utils.reportJSDoc(`Types are not permitted on @${tag.tag}.`, tag, () => {
for (const source of tag.source) {
removeType(source);
}
});
}
}
}, {
contextDefaults: [
'ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression', 'TSDeclareFunction',
// Add this to above defaults
'TSMethodSignature'
],
meta: {
docs: {
description: 'This rule reports types being used on `@param` or `@returns`.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-types.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,380 @@
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { createSyncFn } from 'synckit';
import {
getJSDocComment,
parse as parseType,
traverse,
tryParse as tryParseType,
} from '@es-joy/jsdoccomment';
import iterateJsdoc, {
parseComment,
} from '../iterateJsdoc.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pathName = join(__dirname, '../import-worker.mjs');
const extraTypes = [
'null', 'undefined', 'void', 'string', 'boolean', 'object',
'function', 'symbol',
'number', 'bigint', 'NaN', 'Infinity',
'any', '*', 'never', 'unknown', 'const',
'this', 'true', 'false',
'Array', 'Object', 'RegExp', 'Date', 'Function',
];
const typescriptGlobals = [
// https://www.typescriptlang.org/docs/handbook/utility-types.html
'Awaited',
'Partial',
'Required',
'Readonly',
'Record',
'Pick',
'Omit',
'Exclude',
'Extract',
'NonNullable',
'Parameters',
'ConstructorParameters',
'ReturnType',
'InstanceType',
'ThisParameterType',
'OmitThisParameter',
'ThisType',
'Uppercase',
'Lowercase',
'Capitalize',
'Uncapitalize',
];
/**
* @param {string|false|undefined} [str]
* @returns {undefined|string|false}
*/
const stripPseudoTypes = (str) => {
return str && str.replace(/(?:\.|<>|\.<>|\[\])$/u, '');
};
export default iterateJsdoc(({
context,
node,
report,
settings,
sourceCode,
utils,
}) => {
const {
scopeManager,
} = sourceCode;
// When is this ever `null`?
const globalScope = /** @type {import('eslint').Scope.Scope} */ (
scopeManager.globalScope
);
const
/**
* @type {{
* definedTypes: string[],
* disableReporting: boolean,
* markVariablesAsUsed: boolean
* }}
*/ {
definedTypes = [],
disableReporting = false,
markVariablesAsUsed = true,
} = context.options[0] || {};
/** @type {(string|undefined)[]} */
let definedPreferredTypes = [];
const {
preferredTypes,
structuredTags,
mode,
} = settings;
if (Object.keys(preferredTypes).length) {
definedPreferredTypes = /** @type {string[]} */ (Object.values(preferredTypes).map((preferredType) => {
if (typeof preferredType === 'string') {
// May become an empty string but will be filtered out below
return stripPseudoTypes(preferredType);
}
if (!preferredType) {
return undefined;
}
if (typeof preferredType !== 'object') {
utils.reportSettings(
'Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.',
);
}
return stripPseudoTypes(preferredType.replacement);
})
.filter(Boolean));
}
const comments = sourceCode.getAllComments()
.filter((comment) => {
return (/^\*\s/u).test(comment.value);
})
.map((commentNode) => {
return parseComment(commentNode, '');
});
const typedefDeclarations = comments
.flatMap((doc) => {
return doc.tags.filter(({
tag,
}) => {
return utils.isNamepathDefiningTag(tag);
});
})
.map((tag) => {
return tag.name;
});
const importTags = settings.mode === 'typescript' ? /** @type {string[]} */ (comments.flatMap((doc) => {
return doc.tags.filter(({
tag,
}) => {
return tag === 'import';
});
}).flatMap((tag) => {
const {
type, name, description
} = tag;
const typePart = type ? `{${type}} `: '';
const imprt = 'import ' + (description
? `${typePart}${name} ${description}`
: `${typePart}${name}`);
const getImports = createSyncFn(pathName);
const imports = /** @type {import('parse-imports').Import[]} */ (getImports(imprt));
if (!imports) {
return null;
}
return imports.flatMap(({importClause}) => {
/* c8 ignore next */
const {default: dflt, named, namespace} = importClause || {};
const types = [];
if (dflt) {
types.push(dflt);
}
if (namespace) {
types.push(namespace);
}
if (named) {
for (const {binding} of named) {
types.push(binding);
}
}
return types;
});
}).filter(Boolean)) : [];
const ancestorNodes = [];
let currentNode = node;
// No need for Program node?
while (currentNode?.parent) {
ancestorNodes.push(currentNode);
currentNode = currentNode.parent;
}
/**
* @param {import('eslint').Rule.Node} ancestorNode
* @returns {import('comment-parser').Spec[]}
*/
const getTemplateTags = function (ancestorNode) {
const commentNode = getJSDocComment(sourceCode, ancestorNode, settings);
if (!commentNode) {
return [];
}
const jsdoc = parseComment(commentNode, '');
return jsdoc.tags.filter((tag) => {
return tag.tag === 'template';
});
};
// `currentScope` may be `null` or `Program`, so in such a case,
// we look to present tags instead
const templateTags = ancestorNodes.length ?
ancestorNodes.flatMap((ancestorNode) => {
return getTemplateTags(ancestorNode);
}) :
utils.getPresentTags([
'template',
]);
const closureGenericTypes = templateTags.flatMap((tag) => {
return utils.parseClosureTemplateTag(tag);
});
// In modules, including Node, there is a global scope at top with the
// Program scope inside
const cjsOrESMScope = globalScope.childScopes[0]?.block?.type === 'Program';
const allDefinedTypes = new Set(globalScope.variables.map(({
name,
}) => {
return name;
})
// If the file is a module, concat the variables from the module scope.
.concat(
cjsOrESMScope ?
globalScope.childScopes.flatMap(({
variables,
}) => {
return variables;
}).map(({
name,
}) => {
return name;
/* c8 ignore next */
}) : [],
)
.concat(extraTypes)
.concat(typedefDeclarations)
.concat(importTags)
.concat(definedTypes)
.concat(/** @type {string[]} */ (definedPreferredTypes))
.concat(
settings.mode === 'jsdoc' ?
[] :
[
...settings.mode === 'typescript' ? typescriptGlobals : [],
...closureGenericTypes,
],
));
/**
* @typedef {{
* parsedType: import('jsdoc-type-pratt-parser').RootResult;
* tag: import('comment-parser').Spec|import('@es-joy/jsdoccomment').JsdocInlineTagNoType & {
* line?: import('../iterateJsdoc.js').Integer
* }
* }} TypeAndTagInfo
*/
/**
* @param {string} propertyName
* @returns {(tag: (import('@es-joy/jsdoccomment').JsdocInlineTagNoType & {
* name?: string,
* type?: string,
* line?: import('../iterateJsdoc.js').Integer
* })|import('comment-parser').Spec & {
* namepathOrURL?: string
* }
* ) => undefined|TypeAndTagInfo}
*/
const tagToParsedType = (propertyName) => {
return (tag) => {
try {
const potentialType = tag[
/** @type {"type"|"name"|"namepathOrURL"} */ (propertyName)
];
return {
parsedType: mode === 'permissive' ?
tryParseType(/** @type {string} */ (potentialType)) :
parseType(/** @type {string} */ (potentialType), mode),
tag,
};
} catch {
return undefined;
}
};
};
const typeTags = utils.filterTags(({
tag,
}) => {
return tag !== 'import' && utils.tagMightHaveTypePosition(tag) && (tag !== 'suppress' || settings.mode !== 'closure');
}).map(tagToParsedType('type'));
const namepathReferencingTags = utils.filterTags(({
tag,
}) => {
return utils.isNamepathReferencingTag(tag);
}).map(tagToParsedType('name'));
const namepathOrUrlReferencingTags = utils.filterAllTags(({
tag,
}) => {
return utils.isNamepathOrUrlReferencingTag(tag);
}).map(tagToParsedType('namepathOrURL'));
const tagsWithTypes = /** @type {TypeAndTagInfo[]} */ ([
...typeTags,
...namepathReferencingTags,
...namepathOrUrlReferencingTags,
// Remove types which failed to parse
].filter(Boolean));
for (const {
tag,
parsedType,
} of tagsWithTypes) {
traverse(parsedType, (nde) => {
const {
type,
value,
} = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde);
if (type === 'JsdocTypeName') {
const structuredTypes = structuredTags[tag.tag]?.type;
if (!allDefinedTypes.has(value) &&
(!Array.isArray(structuredTypes) || !structuredTypes.includes(value))
) {
if (!disableReporting) {
report(`The type '${value}' is undefined.`, null, tag);
}
} else if (markVariablesAsUsed && !extraTypes.includes(value)) {
if (sourceCode.markVariableAsUsed) {
sourceCode.markVariableAsUsed(value);
/* c8 ignore next 3 */
} else {
context.markVariableAsUsed(value);
}
}
}
});
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Checks that types in jsdoc comments are defined.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-undefined-types.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
definedTypes: {
items: {
type: 'string',
},
type: 'array',
},
disableReporting: {
type: 'boolean',
},
markVariablesAsUsed: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,189 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
jsdoc,
utils,
indent,
}) => {
const [
defaultRequireValue = 'always',
{
tags: tagMap = {},
} = {},
] = context.options;
const {
source,
} = jsdoc;
const always = defaultRequireValue === 'always';
const never = defaultRequireValue === 'never';
/** @type {string} */
let currentTag;
source.some(({
number,
tokens,
}) => {
const {
delimiter,
tag,
end,
description,
} = tokens;
/**
* @returns {void}
*/
const neverFix = () => {
tokens.delimiter = '';
tokens.postDelimiter = '';
};
/**
* @param {string} checkValue
* @returns {boolean}
*/
const checkNever = (checkValue) => {
if (delimiter && delimiter !== '/**' && (
never && !tagMap.always?.includes(checkValue) ||
tagMap.never?.includes(checkValue)
)) {
utils.reportJSDoc('Expected JSDoc line to have no prefix.', {
column: 0,
line: number,
}, neverFix);
return true;
}
return false;
};
/**
* @returns {void}
*/
const alwaysFix = () => {
if (!tokens.start) {
tokens.start = indent + ' ';
}
tokens.delimiter = '*';
tokens.postDelimiter = tag || description ? ' ' : '';
};
/**
* @param {string} checkValue
* @returns {boolean}
*/
const checkAlways = (checkValue) => {
if (
!delimiter && (
always && !tagMap.never?.includes(checkValue) ||
tagMap.always?.includes(checkValue)
)
) {
utils.reportJSDoc('Expected JSDoc line to have the prefix.', {
column: 0,
line: number,
}, alwaysFix);
return true;
}
return false;
};
if (tag) {
// Remove at sign
currentTag = tag.slice(1);
}
if (
// If this is the end but has a tag, the delimiter will also be
// populated and will be safely ignored later
end && !tag
) {
return false;
}
if (!currentTag) {
if (tagMap.any?.includes('*description')) {
return false;
}
if (checkNever('*description')) {
return true;
}
if (checkAlways('*description')) {
return true;
}
return false;
}
if (tagMap.any?.includes(currentTag)) {
return false;
}
if (checkNever(currentTag)) {
return true;
}
if (checkAlways(currentTag)) {
return true;
}
return false;
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description:
'Requires that each JSDoc line starts with an `*`.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-asterisk-prefix.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
enum: [
'always', 'never', 'any',
],
type: 'string',
},
{
additionalProperties: false,
properties: {
tags: {
properties: {
always: {
items: {
type: 'string',
},
type: 'array',
},
any: {
items: {
type: 'string',
},
type: 'array',
},
never: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
},
type: 'object',
},
],
type: 'layout',
},
});

View File

@@ -0,0 +1,161 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {string} description
* @returns {import('../iterateJsdoc.js').Integer}
*/
const checkDescription = (description) => {
return description
.trim()
.split('\n')
.filter(Boolean)
.length;
};
export default iterateJsdoc(({
jsdoc,
report,
utils,
context,
}) => {
if (utils.avoidDocs()) {
return;
}
const {
descriptionStyle = 'body',
} = context.options[0] || {};
let targetTagName = utils.getPreferredTagName({
// We skip reporting except when `@description` is essential to the rule,
// so user can block the tag and still meaningfully use this rule
// even if the tag is present (and `check-tag-names` is the one to
// normally report the fact that it is blocked but present)
skipReportingBlockedTag: descriptionStyle !== 'tag',
tagName: 'description',
});
if (!targetTagName) {
return;
}
const isBlocked = typeof targetTagName === 'object' && 'blocked' in targetTagName && targetTagName.blocked;
if (isBlocked) {
targetTagName = /** @type {{blocked: true; tagName: string;}} */ (
targetTagName
).tagName;
}
if (descriptionStyle !== 'tag') {
const {
description,
} = utils.getDescription();
if (checkDescription(description || '')) {
return;
}
if (descriptionStyle === 'body') {
const descTags = utils.getPresentTags([
'desc', 'description',
]);
if (descTags.length) {
const [
{
tag: tagName,
},
] = descTags;
report(`Remove the @${tagName} tag to leave a plain block description or add additional description text above the @${tagName} line.`);
} else {
report('Missing JSDoc block description.');
}
return;
}
}
const functionExamples = isBlocked ?
[] :
jsdoc.tags.filter(({
tag,
}) => {
return tag === targetTagName;
});
if (!functionExamples.length) {
report(
descriptionStyle === 'any' ?
`Missing JSDoc block description or @${targetTagName} declaration.` :
`Missing JSDoc @${targetTagName} declaration.`,
);
return;
}
for (const example of functionExamples) {
if (!checkDescription(`${example.name} ${utils.getTagDescription(example)}`)) {
report(`Missing JSDoc @${targetTagName} description.`, null, example);
}
}
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that all functions have a description.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-description.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
checkConstructors: {
default: true,
type: 'boolean',
},
checkGetters: {
default: true,
type: 'boolean',
},
checkSetters: {
default: true,
type: 'boolean',
},
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
descriptionStyle: {
enum: [
'body', 'tag', 'any',
],
type: 'string',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,335 @@
import iterateJsdoc from '../iterateJsdoc.js';
import escapeStringRegexp from 'escape-string-regexp';
const otherDescriptiveTags = new Set([
// 'copyright' and 'see' might be good addition, but as the former may be
// sensitive text, and the latter may have just a link, they are not
// included by default
'summary', 'file', 'fileoverview', 'overview', 'classdesc', 'todo',
'deprecated', 'throws', 'exception', 'yields', 'yield',
]);
/**
* @param {string} text
* @returns {string[]}
*/
const extractParagraphs = (text) => {
return text.split(/(?<![;:])\n\n+/u);
};
/**
* @param {string} text
* @param {string|RegExp} abbreviationsRegex
* @returns {string[]}
*/
const extractSentences = (text, abbreviationsRegex) => {
const txt = text
// Remove all {} tags.
.replaceAll(/(?<!^)\{[\s\S]*?\}\s*/gu, '')
// Remove custom abbreviations
.replace(abbreviationsRegex, '');
const sentenceEndGrouping = /([.?!])(?:\s+|$)/ug;
const puncts = [
...txt.matchAll(sentenceEndGrouping),
].map((sentEnd) => {
return sentEnd[0];
});
return txt
.split(/[.?!](?:\s+|$)/u)
// Re-add the dot.
.map((sentence, idx) => {
return !puncts[idx] && /^\s*$/u.test(sentence) ? sentence : `${sentence}${puncts[idx] || ''}`;
});
};
/**
* @param {string} text
* @returns {boolean}
*/
const isNewLinePrecededByAPeriod = (text) => {
/** @type {boolean} */
let lastLineEndsSentence;
const lines = text.split('\n');
return !lines.some((line) => {
if (lastLineEndsSentence === false && /^[A-Z][a-z]/u.test(line)) {
return true;
}
lastLineEndsSentence = /[.:?!|]$/u.test(line);
return false;
});
};
/**
* @param {string} str
* @returns {boolean}
*/
const isCapitalized = (str) => {
return str[0] === str[0].toUpperCase();
};
/**
* @param {string} str
* @returns {boolean}
*/
const isTable = (str) => {
return str.charAt(0) === '|';
};
/**
* @param {string} str
* @returns {string}
*/
const capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
/**
* @param {string} description
* @param {import('../iterateJsdoc.js').Report} reportOrig
* @param {import('eslint').Rule.Node} jsdocNode
* @param {string|RegExp} abbreviationsRegex
* @param {import('eslint').SourceCode} sourceCode
* @param {import('comment-parser').Spec|{
* line: import('../iterateJsdoc.js').Integer
* }} tag
* @param {boolean} newlineBeforeCapsAssumesBadSentenceEnd
* @returns {boolean}
*/
const validateDescription = (
description, reportOrig, jsdocNode, abbreviationsRegex,
sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd,
) => {
if (!description || (/^\n+$/u).test(description)) {
return false;
}
const descriptionNoHeadings = description.replaceAll(/^\s*#[^\n]*(\n|$)/gm, '');
const paragraphs = extractParagraphs(descriptionNoHeadings).filter(Boolean);
return paragraphs.some((paragraph, parIdx) => {
const sentences = extractSentences(paragraph, abbreviationsRegex);
const fix = /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
let text = sourceCode.getText(jsdocNode);
if (!/[.:?!]$/u.test(paragraph)) {
const line = paragraph.split('\n').filter(Boolean).pop();
text = text.replace(new RegExp(`${escapeStringRegexp(
/** @type {string} */
(line),
)}$`, 'mu'), `${line}.`);
}
for (const sentence of sentences.filter((sentence_) => {
return !(/^\s*$/u).test(sentence_) && !isCapitalized(sentence_) &&
!isTable(sentence_);
})) {
const beginning = sentence.split('\n')[0];
if ('tag' in tag && tag.tag) {
const reg = new RegExp(`(@${escapeStringRegexp(tag.tag)}.*)${escapeStringRegexp(beginning)}`, 'u');
text = text.replace(reg, (_$0, $1) => {
return $1 + capitalize(beginning);
});
} else {
text = text.replace(new RegExp('((?:[.?!]|\\*|\\})\\s*)' + escapeStringRegexp(beginning), 'u'), '$1' + capitalize(beginning));
}
}
return fixer.replaceText(jsdocNode, text);
};
/**
* @param {string} msg
* @param {import('eslint').Rule.ReportFixer | null | undefined} fixer
* @param {{
* line?: number | undefined;
* column?: number | undefined;
* } | (import('comment-parser').Spec & {
* line?: number | undefined;
* column?: number | undefined;
* })} tagObj
* @returns {void}
*/
const report = (msg, fixer, tagObj) => {
if ('line' in tagObj) {
/**
* @type {{
* line: number;
* }}
*/ (tagObj).line += parIdx * 2;
} else {
/** @type {import('comment-parser').Spec} */ (
tagObj
).source[0].number += parIdx * 2;
}
// Avoid errors if old column doesn't exist here
tagObj.column = 0;
reportOrig(msg, fixer, tagObj);
};
if (sentences.some((sentence) => {
return (/^[.?!]$/u).test(sentence);
})) {
report('Sentences must be more than punctuation.', null, tag);
}
if (sentences.some((sentence) => {
return !(/^\s*$/u).test(sentence) && !isCapitalized(sentence) && !isTable(sentence);
})) {
report('Sentences should start with an uppercase character.', fix, tag);
}
const paragraphNoAbbreviations = paragraph.replace(abbreviationsRegex, '');
if (!/(?:[.?!|]|```)\s*$/u.test(paragraphNoAbbreviations)) {
report('Sentences must end with a period.', fix, tag);
return true;
}
if (newlineBeforeCapsAssumesBadSentenceEnd && !isNewLinePrecededByAPeriod(paragraphNoAbbreviations)) {
report('A line of text is started with an uppercase character, but the preceding line does not end the sentence.', null, tag);
return true;
}
return false;
});
};
export default iterateJsdoc(({
sourceCode,
context,
jsdoc,
report,
jsdocNode,
utils,
}) => {
const /** @type {{abbreviations: string[], newlineBeforeCapsAssumesBadSentenceEnd: boolean}} */ {
abbreviations = [],
newlineBeforeCapsAssumesBadSentenceEnd = false,
} = context.options[0] || {};
const abbreviationsRegex = abbreviations.length ?
new RegExp('\\b' + abbreviations.map((abbreviation) => {
return escapeStringRegexp(abbreviation.replaceAll(/\.$/ug, '') + '.');
}).join('|') + '(?:$|\\s)', 'gu') :
'';
let {
description,
} = utils.getDescription();
const indices = [
...description.matchAll(/```[\s\S]*```/gu),
].map((match) => {
const {
index,
} = match;
const [
{
length,
},
] = match;
return {
index,
length,
};
}).reverse();
for (const {
index,
length,
} of indices) {
description = description.slice(0, index) +
description.slice(/** @type {import('../iterateJsdoc.js').Integer} */ (
index
) + length);
}
if (validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, {
line: jsdoc.source[0].number + 1,
}, newlineBeforeCapsAssumesBadSentenceEnd)) {
return;
}
utils.forEachPreferredTag('description', (matchingJsdocTag) => {
const desc = `${matchingJsdocTag.name} ${utils.getTagDescription(matchingJsdocTag)}`.trim();
validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, matchingJsdocTag, newlineBeforeCapsAssumesBadSentenceEnd);
}, true);
const {
tagsWithNames,
} = utils.getTagsByType(jsdoc.tags);
const tagsWithoutNames = utils.filterTags(({
tag: tagName,
}) => {
return otherDescriptiveTags.has(tagName) ||
utils.hasOptionTag(tagName) && !tagsWithNames.some(({
tag,
}) => {
// If user accidentally adds tags with names (or like `returns`
// get parsed as having names), do not add to this list
return tag === tagName;
});
});
tagsWithNames.some((tag) => {
const desc = /** @type {string} */ (
utils.getTagDescription(tag)
).replace(/^- /u, '').trimEnd();
return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd);
});
tagsWithoutNames.some((tag) => {
const desc = `${tag.name} ${utils.getTagDescription(tag)}`.trim();
return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd);
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires that block description, explicit `@description`, and `@param`/`@returns` tag descriptions are written in complete sentences.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-description-complete-sentence.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
abbreviations: {
items: {
type: 'string',
},
type: 'array',
},
newlineBeforeCapsAssumesBadSentenceEnd: {
type: 'boolean',
},
tags: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,118 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
jsdoc,
report,
utils,
}) => {
if (utils.avoidDocs()) {
return;
}
const {
enableFixer = true,
exemptNoArguments = false,
} = context.options[0] || {};
const targetTagName = 'example';
const functionExamples = jsdoc.tags.filter(({
tag,
}) => {
return tag === targetTagName;
});
if (!functionExamples.length) {
if (exemptNoArguments && utils.isIteratingFunction() &&
!utils.hasParams()
) {
return;
}
utils.reportJSDoc(`Missing JSDoc @${targetTagName} declaration.`, null, () => {
if (enableFixer) {
utils.addTag(targetTagName);
}
});
return;
}
for (const example of functionExamples) {
const exampleContent = `${example.name} ${utils.getTagDescription(example)}`
.trim()
.split('\n')
.filter(Boolean);
if (!exampleContent.length) {
report(`Missing JSDoc @${targetTagName} description.`, null, example);
}
}
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that all functions have examples.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-example.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
checkConstructors: {
default: true,
type: 'boolean',
},
checkGetters: {
default: false,
type: 'boolean',
},
checkSetters: {
default: false,
type: 'boolean',
},
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
enableFixer: {
default: true,
type: 'boolean',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
exemptNoArguments: {
default: false,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,154 @@
import iterateJsdoc from '../iterateJsdoc.js';
const defaultTags = {
file: {
initialCommentsOnly: true,
mustExist: true,
preventDuplicates: true,
},
};
/**
* @param {import('../iterateJsdoc.js').StateObject} state
* @returns {void}
*/
const setDefaults = (state) => {
// First iteration
if (!state.globalTags) {
state.globalTags = {};
state.hasDuplicates = {};
state.hasTag = {};
state.hasNonCommentBeforeTag = {};
}
};
export default iterateJsdoc(({
jsdocNode,
state,
utils,
context,
}) => {
const {
tags = defaultTags,
} = context.options[0] || {};
setDefaults(state);
for (const tagName of Object.keys(tags)) {
const targetTagName = /** @type {string} */ (utils.getPreferredTagName({
tagName,
}));
const hasTag = Boolean(targetTagName && utils.hasTag(targetTagName));
state.hasTag[tagName] = hasTag || state.hasTag[tagName];
const hasDuplicate = state.hasDuplicates[tagName];
if (hasDuplicate === false) {
// Was marked before, so if a tag now, is a dupe
state.hasDuplicates[tagName] = hasTag;
} else if (!hasDuplicate && hasTag) {
// No dupes set before, but has first tag, so change state
// from `undefined` to `false` so can detect next time
state.hasDuplicates[tagName] = false;
state.hasNonCommentBeforeTag[tagName] = state.hasNonComment &&
state.hasNonComment < jsdocNode.range[0];
}
}
}, {
exit ({
context,
state,
utils,
}) {
setDefaults(state);
const {
tags = defaultTags,
} = context.options[0] || {};
for (const [
tagName,
{
mustExist = false,
preventDuplicates = false,
initialCommentsOnly = false,
},
] of Object.entries(tags)) {
const obj = utils.getPreferredTagNameObject({
tagName,
});
if (obj && typeof obj === 'object' && 'blocked' in obj) {
utils.reportSettings(
`\`settings.jsdoc.tagNamePreference\` cannot block @${obj.tagName} ` +
'for the `require-file-overview` rule',
);
} else {
const targetTagName = (
obj && typeof obj === 'object' && obj.replacement
) || obj;
if (mustExist && !state.hasTag[tagName]) {
utils.reportSettings(`Missing @${targetTagName}`);
}
if (preventDuplicates && state.hasDuplicates[tagName]) {
utils.reportSettings(
`Duplicate @${targetTagName}`,
);
}
if (initialCommentsOnly &&
state.hasNonCommentBeforeTag[tagName]
) {
utils.reportSettings(
`@${targetTagName} should be at the beginning of the file`,
);
}
}
}
},
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Checks that all files have one `@file`, `@fileoverview`, or `@overview` tag at the beginning of the file.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-file-overview.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
tags: {
patternProperties: {
'.*': {
additionalProperties: false,
properties: {
initialCommentsOnly: {
type: 'boolean',
},
mustExist: {
type: 'boolean',
},
preventDuplicates: {
type: 'boolean',
},
},
type: 'object',
},
},
type: 'object',
},
},
type: 'object',
},
],
type: 'suggestion',
},
nonComment ({
state,
node,
}) {
if (!state.hasNonComment) {
state.hasNonComment = node.range[0];
}
},
});

View File

@@ -0,0 +1,178 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
sourceCode,
utils,
report,
context,
jsdoc,
jsdocNode,
}) => {
const [
mainCircumstance,
{
tags = null,
} = {},
] = context.options;
const tgs = /**
* @type {null|"any"|{[key: string]: "always"|"never"}}
*/ (tags);
/**
* @param {import('@es-joy/jsdoccomment').JsdocTagWithInline} jsdocTag
* @param {string} targetTagName
* @param {"always"|"never"} [circumstance]
* @returns {void}
*/
const checkHyphens = (jsdocTag, targetTagName, circumstance = mainCircumstance) => {
const always = !circumstance || circumstance === 'always';
const desc = /** @type {string} */ (utils.getTagDescription(jsdocTag));
if (!desc.trim()) {
return;
}
const startsWithHyphen = (/^\s*-/u).test(desc);
if (always) {
if (!startsWithHyphen) {
report(`There must be a hyphen before @${targetTagName} description.`, (fixer) => {
const lineIndex = /** @type {import('../iterateJsdoc.js').Integer} */ (
jsdocTag.line
);
const sourceLines = sourceCode.getText(jsdocNode).split('\n');
// Get start index of description, accounting for multi-line descriptions
const description = desc.split('\n')[0];
const descriptionIndex = sourceLines[lineIndex].lastIndexOf(description);
const replacementLine = sourceLines[lineIndex]
.slice(0, descriptionIndex) + '- ' + description;
sourceLines.splice(lineIndex, 1, replacementLine);
const replacement = sourceLines.join('\n');
return fixer.replaceText(jsdocNode, replacement);
}, jsdocTag);
}
} else if (startsWithHyphen) {
let lines = 0;
for (const {
tokens,
} of jsdocTag.source) {
if (tokens.description) {
break;
}
lines++;
}
utils.reportJSDoc(
`There must be no hyphen before @${targetTagName} description.`,
{
line: jsdocTag.source[0].number + lines,
},
() => {
for (const {
tokens,
} of jsdocTag.source) {
if (tokens.description) {
tokens.description = tokens.description.replace(
/^\s*-\s*/u, '',
);
break;
}
}
},
true,
);
}
};
utils.forEachPreferredTag('param', checkHyphens);
if (tgs) {
const tagEntries = Object.entries(tgs);
for (const [
tagName,
circumstance,
] of tagEntries) {
if (tagName === '*') {
const preferredParamTag = utils.getPreferredTagName({
tagName: 'param',
});
for (const {
tag,
} of jsdoc.tags) {
if (tag === preferredParamTag || tagEntries.some(([
tagNme,
]) => {
return tagNme !== '*' && tagNme === tag;
})) {
continue;
}
utils.forEachPreferredTag(tag, (jsdocTag, targetTagName) => {
checkHyphens(
jsdocTag,
targetTagName,
/** @type {"always"|"never"} */ (circumstance),
);
});
}
continue;
}
utils.forEachPreferredTag(tagName, (jsdocTag, targetTagName) => {
checkHyphens(
jsdocTag,
targetTagName,
/** @type {"always"|"never"} */ (circumstance),
);
});
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires a hyphen before the `@param` description.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-hyphen-before-param-description.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
enum: [
'always', 'never',
],
type: 'string',
},
{
additionalProperties: false,
properties: {
tags: {
anyOf: [
{
patternProperties: {
'.*': {
enum: [
'always', 'never',
],
type: 'string',
},
},
type: 'object',
},
{
enum: [
'any',
],
type: 'string',
},
],
},
},
type: 'object',
},
],
type: 'layout',
},
});

View File

@@ -0,0 +1,645 @@
import exportParser from '../exportParser.js';
import {
getSettings,
} from '../iterateJsdoc.js';
import {
exemptSpeciaMethods,
isConstructor,
getFunctionParameterNames,
hasReturnValue,
getIndent,
getContextObject,
enforcedContexts,
} from '../jsdocUtils.js';
import {
getDecorator,
getJSDocComment,
getReducedASTNode,
} from '@es-joy/jsdoccomment';
/**
* @typedef {{
* ancestorsOnly: boolean,
* esm: boolean,
* initModuleExports: boolean,
* initWindow: boolean
* }} RequireJsdocOpts
*/
/**
* @typedef {import('eslint').Rule.Node|
* import('@typescript-eslint/types').TSESTree.Node} ESLintOrTSNode
*/
/** @type {import('json-schema').JSONSchema4} */
const OPTIONS_SCHEMA = {
additionalProperties: false,
properties: {
checkConstructors: {
default: true,
type: 'boolean',
},
checkGetters: {
anyOf: [
{
type: 'boolean',
},
{
enum: [
'no-setter',
],
type: 'string',
},
],
default: true,
},
checkSetters: {
anyOf: [
{
type: 'boolean',
},
{
enum: [
'no-getter',
],
type: 'string',
},
],
default: true,
},
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
context: {
type: 'string',
},
inlineCommentBlock: {
type: 'boolean',
},
minLineCount: {
type: 'integer',
},
},
type: 'object',
},
],
},
type: 'array',
},
enableFixer: {
default: true,
type: 'boolean',
},
exemptEmptyConstructors: {
default: false,
type: 'boolean',
},
exemptEmptyFunctions: {
default: false,
type: 'boolean',
},
fixerMessage: {
default: '',
type: 'string',
},
minLineCount: {
type: 'integer',
},
publicOnly: {
oneOf: [
{
default: false,
type: 'boolean',
},
{
additionalProperties: false,
default: {},
properties: {
ancestorsOnly: {
type: 'boolean',
},
cjs: {
type: 'boolean',
},
esm: {
type: 'boolean',
},
window: {
type: 'boolean',
},
},
type: 'object',
},
],
},
require: {
additionalProperties: false,
default: {},
properties: {
ArrowFunctionExpression: {
default: false,
type: 'boolean',
},
ClassDeclaration: {
default: false,
type: 'boolean',
},
ClassExpression: {
default: false,
type: 'boolean',
},
FunctionDeclaration: {
default: true,
type: 'boolean',
},
FunctionExpression: {
default: false,
type: 'boolean',
},
MethodDefinition: {
default: false,
type: 'boolean',
},
},
type: 'object',
},
},
type: 'object',
};
/**
* @param {import('eslint').Rule.RuleContext} context
* @param {import('json-schema').JSONSchema4Object} baseObject
* @param {string} option
* @param {string} key
* @returns {boolean|undefined}
*/
const getOption = (context, baseObject, option, key) => {
if (context.options[0] && option in context.options[0] &&
// Todo: boolean shouldn't be returning property, but
// tests currently require
(typeof context.options[0][option] === 'boolean' ||
key in context.options[0][option])
) {
return context.options[0][option][key];
}
return /** @type {{[key: string]: {default?: boolean|undefined}}} */ (
baseObject.properties
)[key].default;
};
/**
* @param {import('eslint').Rule.RuleContext} context
* @param {import('../iterateJsdoc.js').Settings} settings
* @returns {{
* contexts: (string|{
* context: string,
* inlineCommentBlock: boolean,
* minLineCount: import('../iterateJsdoc.js').Integer
* })[],
* enableFixer: boolean,
* exemptEmptyConstructors: boolean,
* exemptEmptyFunctions: boolean,
* fixerMessage: string,
* minLineCount: undefined|import('../iterateJsdoc.js').Integer,
* publicOnly: boolean|{[key: string]: boolean|undefined}
* require: {[key: string]: boolean|undefined}
* }}
*/
const getOptions = (context, settings) => {
const {
publicOnly,
contexts = settings.contexts || [],
exemptEmptyConstructors = true,
exemptEmptyFunctions = false,
enableFixer = true,
fixerMessage = '',
minLineCount = undefined,
} = context.options[0] || {};
return {
contexts,
enableFixer,
exemptEmptyConstructors,
exemptEmptyFunctions,
fixerMessage,
minLineCount,
publicOnly: ((baseObj) => {
if (!publicOnly) {
return false;
}
/** @type {{[key: string]: boolean|undefined}} */
const properties = {};
for (const prop of Object.keys(
/** @type {import('json-schema').JSONSchema4Object} */ (
/** @type {import('json-schema').JSONSchema4Object} */ (
baseObj
).properties),
)) {
const opt = getOption(
context,
/** @type {import('json-schema').JSONSchema4Object} */ (baseObj),
'publicOnly',
prop,
);
properties[prop] = opt;
}
return properties;
})(
/** @type {import('json-schema').JSONSchema4Object} */
(
/** @type {import('json-schema').JSONSchema4Object} */
(
/** @type {import('json-schema').JSONSchema4Object} */
(
OPTIONS_SCHEMA.properties
).publicOnly
).oneOf
)[1],
),
require: ((baseObj) => {
/** @type {{[key: string]: boolean|undefined}} */
const properties = {};
for (const prop of Object.keys(
/** @type {import('json-schema').JSONSchema4Object} */ (
/** @type {import('json-schema').JSONSchema4Object} */ (
baseObj
).properties),
)) {
const opt = getOption(
context,
/** @type {import('json-schema').JSONSchema4Object} */
(baseObj),
'require',
prop,
);
properties[prop] = opt;
}
return properties;
})(
/** @type {import('json-schema').JSONSchema4Object} */
(OPTIONS_SCHEMA.properties).require,
),
};
};
/** @type {import('eslint').Rule.RuleModule} */
export default {
create (context) {
/* c8 ignore next -- Fallback to deprecated method */
const {
sourceCode = context.getSourceCode(),
} = context;
const settings = getSettings(context);
if (!settings) {
return {};
}
const opts = getOptions(context, settings);
const {
require: requireOption,
contexts,
exemptEmptyFunctions,
exemptEmptyConstructors,
enableFixer,
fixerMessage,
minLineCount,
} = opts;
const publicOnly =
/**
* @type {{
* [key: string]: boolean | undefined;
* }}
*/ (
opts.publicOnly
);
/**
* @type {import('../iterateJsdoc.js').CheckJsdoc}
*/
const checkJsDoc = (info, _handler, node) => {
if (
// Optimize
minLineCount !== undefined || contexts.some((ctxt) => {
if (typeof ctxt === 'string') {
return false;
}
const {
minLineCount: count,
} = ctxt;
return count !== undefined;
})
) {
/**
* @param {undefined|import('../iterateJsdoc.js').Integer} count
*/
const underMinLine = (count) => {
return count !== undefined && count >
(sourceCode.getText(node).match(/\n/gu)?.length ?? 0) + 1;
};
if (underMinLine(minLineCount)) {
return;
}
const {
minLineCount: contextMinLineCount,
} =
/**
* @type {{
* context: string;
* inlineCommentBlock: boolean;
* minLineCount: number;
* }}
*/ (contexts.find((ctxt) => {
if (typeof ctxt === 'string') {
return false;
}
const {
context: ctx,
} = ctxt;
return ctx === (info.selector || node.type);
})) || {};
if (underMinLine(contextMinLineCount)) {
return;
}
}
const jsDocNode = getJSDocComment(sourceCode, node, settings);
if (jsDocNode) {
return;
}
// For those who have options configured against ANY constructors (or
// setters or getters) being reported
if (exemptSpeciaMethods(
{
description: '',
inlineTags: [],
problems: [],
source: [],
tags: [],
},
node,
context,
[
OPTIONS_SCHEMA,
],
)) {
return;
}
if (
// Avoid reporting param-less, return-less functions (when
// `exemptEmptyFunctions` option is set)
exemptEmptyFunctions && info.isFunctionContext ||
// Avoid reporting param-less, return-less constructor methods (when
// `exemptEmptyConstructors` option is set)
exemptEmptyConstructors && isConstructor(node)
) {
const functionParameterNames = getFunctionParameterNames(node);
if (!functionParameterNames.length && !hasReturnValue(node)) {
return;
}
}
const fix = /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
// Default to one line break if the `minLines`/`maxLines` settings allow
const lines = settings.minLines === 0 && settings.maxLines >= 1 ? 1 : settings.minLines;
/** @type {ESLintOrTSNode|import('@typescript-eslint/types').TSESTree.Decorator} */
let baseNode = getReducedASTNode(node, sourceCode);
const decorator = getDecorator(
/** @type {import('eslint').Rule.Node} */
(baseNode)
);
if (decorator) {
baseNode = decorator;
}
const indent = getIndent({
text: sourceCode.getText(
/** @type {import('eslint').Rule.Node} */ (baseNode),
/** @type {import('eslint').AST.SourceLocation} */
(
/** @type {import('eslint').Rule.Node} */ (baseNode).loc
).start.column,
),
});
const {
inlineCommentBlock,
} =
/**
* @type {{
* context: string,
* inlineCommentBlock: boolean,
* minLineCount: import('../iterateJsdoc.js').Integer
* }}
*/ (contexts.find((contxt) => {
if (typeof contxt === 'string') {
return false;
}
const {
context: ctxt,
} = contxt;
return ctxt === node.type;
})) || {};
const insertion = (inlineCommentBlock ?
`/** ${fixerMessage}` :
`/**\n${indent}*${fixerMessage}\n${indent}`) +
`*/${'\n'.repeat(lines)}${indent.slice(0, -1)}`;
return fixer.insertTextBefore(
/** @type {import('eslint').Rule.Node} */
(baseNode),
insertion,
);
};
const report = () => {
const {
start,
} = /** @type {import('eslint').AST.SourceLocation} */ (node.loc);
const loc = {
end: {
column: 0,
line: start.line + 1,
},
start,
};
context.report({
fix: enableFixer ? fix : null,
loc,
messageId: 'missingJsDoc',
node,
});
};
if (publicOnly) {
/** @type {RequireJsdocOpts} */
const opt = {
ancestorsOnly: Boolean(publicOnly?.ancestorsOnly ?? false),
esm: Boolean(publicOnly?.esm ?? true),
initModuleExports: Boolean(publicOnly?.cjs ?? true),
initWindow: Boolean(publicOnly?.window ?? false),
};
const exported = exportParser.isUncommentedExport(node, sourceCode, opt, settings);
if (exported) {
report();
}
} else {
report();
}
};
/**
* @param {string} prop
* @returns {boolean}
*/
const hasOption = (prop) => {
return requireOption[prop] || contexts.some((ctxt) => {
return typeof ctxt === 'object' ? ctxt.context === prop : ctxt === prop;
});
};
return {
...getContextObject(
enforcedContexts(context, [], settings),
checkJsDoc,
),
ArrowFunctionExpression (node) {
if (!hasOption('ArrowFunctionExpression')) {
return;
}
if (
[
'VariableDeclarator', 'AssignmentExpression', 'ExportDefaultDeclaration',
].includes(node.parent.type) ||
[
'Property', 'ObjectProperty', 'ClassProperty', 'PropertyDefinition',
].includes(node.parent.type) &&
node ===
/**
* @type {import('@typescript-eslint/types').TSESTree.Property|
* import('@typescript-eslint/types').TSESTree.PropertyDefinition
* }
*/
(node.parent).value
) {
checkJsDoc({
isFunctionContext: true,
}, null, node);
}
},
ClassDeclaration (node) {
if (!hasOption('ClassDeclaration')) {
return;
}
checkJsDoc({
isFunctionContext: false,
}, null, node);
},
ClassExpression (node) {
if (!hasOption('ClassExpression')) {
return;
}
checkJsDoc({
isFunctionContext: false,
}, null, node);
},
FunctionDeclaration (node) {
if (!hasOption('FunctionDeclaration')) {
return;
}
checkJsDoc({
isFunctionContext: true,
}, null, node);
},
FunctionExpression (node) {
if (!hasOption('FunctionExpression')) {
return;
}
if (
[
'VariableDeclarator', 'AssignmentExpression', 'ExportDefaultDeclaration',
].includes(node.parent.type) ||
[
'Property', 'ObjectProperty', 'ClassProperty', 'PropertyDefinition',
].includes(node.parent.type) &&
node ===
/**
* @type {import('@typescript-eslint/types').TSESTree.Property|
* import('@typescript-eslint/types').TSESTree.PropertyDefinition
* }
*/
(node.parent).value
) {
checkJsDoc({
isFunctionContext: true,
}, null, node);
}
},
MethodDefinition (node) {
if (!hasOption('MethodDefinition')) {
return;
}
checkJsDoc({
isFunctionContext: true,
selector: 'MethodDefinition',
}, null, /** @type {import('eslint').Rule.Node} */ (node.value));
},
};
},
meta: {
docs: {
category: 'Stylistic Issues',
description: 'Require JSDoc comments',
recommended: true,
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-jsdoc.md#repos-sticky-header',
},
fixable: 'code',
messages: {
missingJsDoc: 'Missing JSDoc comment.',
},
schema: [
OPTIONS_SCHEMA,
],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,594 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @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 */
export default iterateJsdoc(({
jsdoc,
utils,
context,
}) => {
/* eslint-enable complexity -- Temporary */
if (utils.avoidDocs()) {
return;
}
// Param type is specified by type in @type
if (utils.hasTag('type')) {
return;
}
const {
autoIncrementBase = 0,
checkRestProperty = false,
checkDestructured = true,
checkDestructuredRoots = true,
checkTypesPattern = '/^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$/',
enableFixer = true,
enableRootFixer = true,
enableRestElementFixer = true,
unnamedRootBase = [
'root',
],
useDefaultObjectProperties = false,
} = context.options[0] || {};
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));
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 {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 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 {
hasRestElement,
hasPropertyRest,
rests,
names,
} = /**
* @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,
remove,
inc,
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: {
tag,
end,
},
}) => {
return tag || end;
});
if (remove) {
createTokens(functionParameterIdx, offset + functionParameterIdx, 1);
} else {
const expectedIdx = findExpectedIndex(jsdoc.tags, functionParameterIdx);
createTokens(expectedIdx, offset + 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.',
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,
type: 'integer',
},
checkConstructors: {
default: true,
type: 'boolean',
},
checkDestructured: {
default: true,
type: 'boolean',
},
checkDestructuredRoots: {
default: true,
type: 'boolean',
},
checkGetters: {
default: false,
type: 'boolean',
},
checkRestProperty: {
default: false,
type: 'boolean',
},
checkSetters: {
default: false,
type: 'boolean',
},
checkTypesPattern: {
type: 'string',
},
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
enableFixer: {
type: 'boolean',
},
enableRestElementFixer: {
type: 'boolean',
},
enableRootFixer: {
type: 'boolean',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
unnamedRootBase: {
items: {
type: 'string',
},
type: 'array',
},
useDefaultObjectProperties: {
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,
});

View File

@@ -0,0 +1,89 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
report,
settings,
utils,
}) => {
const {
defaultDestructuredRootDescription = 'The root object',
setDefaultDestructuredRootDescription = false,
} = context.options[0] || {};
const functionParameterNames = utils.getFunctionParameterNames();
let rootCount = -1;
utils.forEachPreferredTag('param', (jsdocParameter, targetTagName) => {
rootCount += jsdocParameter.name.includes('.') ? 0 : 1;
if (!jsdocParameter.description.trim()) {
if (Array.isArray(functionParameterNames[rootCount])) {
if (settings.exemptDestructuredRootsFromChecks) {
return;
}
if (setDefaultDestructuredRootDescription) {
utils.reportJSDoc(`Missing root description for @${targetTagName}.`, jsdocParameter, () => {
utils.changeTag(jsdocParameter, {
description: defaultDestructuredRootDescription,
postName: ' ',
});
});
return;
}
}
report(
`Missing JSDoc @${targetTagName} "${jsdocParameter.name}" description.`,
null,
jsdocParameter,
);
}
});
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that each `@param` tag has a `description` value.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-param-description.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
defaultDestructuredRootDescription: {
type: 'string',
},
setDefaultDestructuredRootDescription: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,55 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
report,
utils,
}) => {
utils.forEachPreferredTag('param', (jsdocParameter, targetTagName) => {
if (jsdocParameter.tag && jsdocParameter.name === '') {
report(
`There must be an identifier after @${targetTagName} ${jsdocParameter.type === '' ? 'type' : 'tag'}.`,
null,
jsdocParameter,
);
}
});
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that all function parameters have names.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-param-name.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,89 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
report,
settings,
utils,
}) => {
const {
defaultDestructuredRootType = 'object',
setDefaultDestructuredRootType = false,
} = context.options[0] || {};
const functionParameterNames = utils.getFunctionParameterNames();
let rootCount = -1;
utils.forEachPreferredTag('param', (jsdocParameter, targetTagName) => {
rootCount += jsdocParameter.name.includes('.') ? 0 : 1;
if (!jsdocParameter.type) {
if (Array.isArray(functionParameterNames[rootCount])) {
if (settings.exemptDestructuredRootsFromChecks) {
return;
}
if (setDefaultDestructuredRootType) {
utils.reportJSDoc(`Missing root type for @${targetTagName}.`, jsdocParameter, () => {
utils.changeTag(jsdocParameter, {
postType: ' ',
type: `{${defaultDestructuredRootType}}`,
});
});
return;
}
}
report(
`Missing JSDoc @${targetTagName} "${jsdocParameter.name}" type.`,
null,
jsdocParameter,
);
}
});
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that each `@param` tag has a `type` value.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-param-type.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
defaultDestructuredRootType: {
type: 'string',
},
setDefaultDestructuredRootType: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,48 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
utils,
}) => {
const propertyAssociatedTags = utils.filterTags(({
tag,
}) => {
return [
'typedef', 'namespace',
].includes(tag);
});
if (!propertyAssociatedTags.length) {
return;
}
const targetTagName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'property',
}));
if (utils.hasATag([
targetTagName,
])) {
return;
}
for (const propertyAssociatedTag of propertyAssociatedTags) {
if (![
'object', 'Object', 'PlainObject',
].includes(propertyAssociatedTag.type)) {
continue;
}
utils.reportJSDoc(`Missing JSDoc @${targetTagName}.`, null, () => {
utils.addTag(targetTagName);
});
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires that all `@typedef` and `@namespace` tags have `@property` when their type is a plain `object`, `Object`, or `PlainObject`.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-property.md#repos-sticky-header',
},
fixable: 'code',
type: 'suggestion',
},
});

View File

@@ -0,0 +1,25 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
report,
utils,
}) => {
utils.forEachPreferredTag('property', (jsdoc, targetTagName) => {
if (!jsdoc.description.trim()) {
report(
`Missing JSDoc @${targetTagName} "${jsdoc.name}" description.`,
null,
jsdoc,
);
}
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires that each `@property` tag has a `description` value.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-property-description.md#repos-sticky-header',
},
type: 'suggestion',
},
});

View File

@@ -0,0 +1,25 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
report,
utils,
}) => {
utils.forEachPreferredTag('property', (jsdoc, targetTagName) => {
if (jsdoc.tag && jsdoc.name === '') {
report(
`There must be an identifier after @${targetTagName} ${jsdoc.type === '' ? 'type' : 'tag'}.`,
null,
jsdoc,
);
}
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires that all function `@property` tags have names.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-property-name.md#repos-sticky-header',
},
type: 'suggestion',
},
});

View File

@@ -0,0 +1,25 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
report,
utils,
}) => {
utils.forEachPreferredTag('property', (jsdoc, targetTagName) => {
if (!jsdoc.type) {
report(
`Missing JSDoc @${targetTagName} "${jsdoc.name}" type.`,
null,
jsdoc,
);
}
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires that each `@property` tag has a `type` value.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-property-type.md#repos-sticky-header',
},
type: 'suggestion',
},
});

View File

@@ -0,0 +1,238 @@
import exportParser from '../exportParser.js';
import iterateJsdoc from '../iterateJsdoc.js';
/**
* We can skip checking for a return value, in case the documentation is inherited
* or the method is either a constructor or an abstract method.
*
* In either of these cases the return value is optional or not defined.
* @param {import('../iterateJsdoc.js').Utils} utils
* a reference to the utils which are used to probe if a tag is present or not.
* @returns {boolean}
* true in case deep checking can be skipped; otherwise false.
*/
const canSkip = (utils) => {
return utils.hasATag([
// inheritdoc implies that all documentation is inherited
// see https://jsdoc.app/tags-inheritdoc.html
//
// Abstract methods are by definition incomplete,
// so it is not an error if it declares a return value but does not implement it.
'abstract',
'virtual',
// Constructors do not have a return value by definition (https://jsdoc.app/tags-class.html)
// So we can bail out here, too.
'class',
'constructor',
// Return type is specified by type in @type
'type',
// This seems to imply a class as well
'interface',
]) ||
utils.avoidDocs();
};
export default iterateJsdoc(({
info: {
comment,
},
node,
report,
settings,
utils,
context,
}) => {
const {
contexts,
enableFixer = false,
forceRequireReturn = false,
forceReturnsWithAsync = false,
publicOnly = false,
} = context.options[0] || {};
// A preflight check. We do not need to run a deep check
// in case the @returns comment is optional or undefined.
if (canSkip(utils)) {
return;
}
/** @type {boolean|undefined} */
let forceRequireReturnContext;
if (contexts) {
const {
foundContext,
} = utils.findContext(contexts, comment);
if (typeof foundContext === 'object') {
forceRequireReturnContext = foundContext.forceRequireReturn;
}
}
const tagName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'returns',
}));
if (!tagName) {
return;
}
const tags = utils.getTags(tagName);
if (tags.length > 1) {
report(`Found more than one @${tagName} declaration.`);
}
const iteratingFunction = utils.isIteratingFunction();
// In case the code returns something, we expect a return value in JSDoc.
const [
tag,
] = tags;
const missingReturnTag = typeof tag === 'undefined' || tag === null;
const shouldReport = () => {
if (!missingReturnTag) {
return false;
}
if (publicOnly) {
/** @type {import('./requireJsdoc.js').RequireJsdocOpts} */
const opt = {
ancestorsOnly: Boolean(publicOnly?.ancestorsOnly ?? false),
esm: Boolean(publicOnly?.esm ?? true),
initModuleExports: Boolean(publicOnly?.cjs ?? true),
initWindow: Boolean(publicOnly?.window ?? false),
};
/* c8 ignore next -- Fallback to deprecated method */
const {
sourceCode = context.getSourceCode(),
} = context;
const exported = exportParser.isUncommentedExport(
/** @type {import('eslint').Rule.Node} */ (node), sourceCode, opt, settings,
);
if (!exported) {
return false;
}
}
if ((forceRequireReturn || forceRequireReturnContext) && (
iteratingFunction || utils.isVirtualFunction()
)) {
return true;
}
const isAsync = !iteratingFunction && utils.hasTag('async') ||
iteratingFunction && utils.isAsync();
if (forceReturnsWithAsync && isAsync) {
return true;
}
return iteratingFunction && utils.hasValueOrExecutorHasNonEmptyResolveValue(
forceReturnsWithAsync,
);
};
if (shouldReport()) {
utils.reportJSDoc(`Missing JSDoc @${tagName} declaration.`, null, enableFixer ? () => {
utils.addTag(tagName);
} : null);
}
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that returns are documented.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-returns.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
checkConstructors: {
default: false,
type: 'boolean',
},
checkGetters: {
default: true,
type: 'boolean',
},
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
forceRequireReturn: {
type: 'boolean',
},
},
type: 'object',
},
],
},
type: 'array',
},
enableFixer: {
type: 'boolean',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
forceRequireReturn: {
default: false,
type: 'boolean',
},
forceReturnsWithAsync: {
default: false,
type: 'boolean',
},
publicOnly: {
oneOf: [
{
default: false,
type: 'boolean',
},
{
additionalProperties: false,
default: {},
properties: {
ancestorsOnly: {
type: 'boolean',
},
cjs: {
type: 'boolean',
},
esm: {
type: 'boolean',
},
window: {
type: 'boolean',
},
},
type: 'object',
},
],
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,145 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {import('../iterateJsdoc.js').Utils} utils
* @param {import('../iterateJsdoc.js').Settings} settings
* @returns {boolean}
*/
const canSkip = (utils, settings) => {
const voidingTags = [
// An abstract function is by definition incomplete
// so it is perfectly fine if a return is documented but
// not present within the function.
// A subclass may inherit the doc and implement the
// missing return.
'abstract',
'virtual',
// A constructor function returns `this` by default, so may be `@returns`
// tag indicating this but no explicit return
'class',
'constructor',
'interface',
];
if (settings.mode === 'closure') {
// Structural Interface in GCC terms, equivalent to @interface tag as far as this rule is concerned
voidingTags.push('record');
}
return utils.hasATag(voidingTags) ||
utils.isConstructor() ||
utils.classHasTag('interface') ||
settings.mode === 'closure' && utils.classHasTag('record');
};
// eslint-disable-next-line complexity -- Temporary
export default iterateJsdoc(({
context,
node,
report,
settings,
utils,
}) => {
const {
exemptAsync = true,
exemptGenerators = settings.mode === 'typescript',
reportMissingReturnForUndefinedTypes = false,
} = context.options[0] || {};
if (canSkip(utils, settings)) {
return;
}
if (exemptAsync && utils.isAsync()) {
return;
}
const tagName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'returns',
}));
if (!tagName) {
return;
}
const tags = utils.getTags(tagName);
if (tags.length === 0) {
return;
}
if (tags.length > 1) {
report(`Found more than one @${tagName} declaration.`);
return;
}
const [
tag,
] = tags;
const type = tag.type.trim();
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions
if (/asserts\s/u.test(type)) {
return;
}
const returnNever = type === 'never';
if (returnNever && utils.hasValueOrExecutorHasNonEmptyResolveValue(false)) {
report(`JSDoc @${tagName} declaration set with "never" but return expression is present in function.`);
return;
}
// In case a return value is declared in JSDoc, we also expect one in the code.
if (
!returnNever &&
(
reportMissingReturnForUndefinedTypes ||
!utils.mayBeUndefinedTypeTag(tag)
) &&
(tag.type === '' && !utils.hasValueOrExecutorHasNonEmptyResolveValue(
exemptAsync,
) ||
tag.type !== '' && !utils.hasValueOrExecutorHasNonEmptyResolveValue(
exemptAsync,
true,
)) &&
Boolean(
!exemptGenerators || !node ||
!('generator' in /** @type {import('../iterateJsdoc.js').Node} */ (node)) ||
!(/** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (node)).generator
)
) {
report(`JSDoc @${tagName} declaration present but return expression not available in function.`);
}
}, {
meta: {
docs: {
description: 'Requires a return statement in function body if a `@returns` tag is specified in jsdoc comment.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-returns-check.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
exemptAsync: {
default: true,
type: 'boolean',
},
exemptGenerators: {
type: 'boolean',
},
reportMissingReturnForUndefinedTypes: {
default: false,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,59 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
report,
utils,
}) => {
utils.forEachPreferredTag('returns', (jsdocTag, targetTagName) => {
const type = jsdocTag.type && jsdocTag.type.trim();
if ([
'void', 'undefined', 'Promise<void>', 'Promise<undefined>',
].includes(type)) {
return;
}
if (!jsdocTag.description.trim()) {
report(`Missing JSDoc @${targetTagName} description.`, null, jsdocTag);
}
});
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that the `@returns` tag has a `description` value.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-returns-description.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,51 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
report,
utils,
}) => {
utils.forEachPreferredTag('returns', (jsdocTag, targetTagName) => {
if (!jsdocTag.type) {
report(`Missing JSDoc @${targetTagName} type.`, null, jsdocTag);
}
});
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that `@returns` tag has `type` value.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-returns-type.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,180 @@
import {
parse as parseType,
traverse,
tryParse as tryParseType,
} from '@es-joy/jsdoccomment';
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
utils,
node,
settings,
report,
}) => {
const {
requireSeparateTemplates = false,
} = context.options[0] || {};
const {
mode
} = settings;
const usedNames = new Set();
const templateTags = utils.getTags('template');
const templateNames = templateTags.flatMap(({name}) => {
return name.split(/,\s*/);
});
for (const tag of templateTags) {
const {name} = tag;
const names = name.split(/,\s*/);
if (requireSeparateTemplates && names.length > 1) {
report(`Missing separate @template for ${names[1]}`, null, tag);
}
}
/**
* @param {import('@typescript-eslint/types').TSESTree.FunctionDeclaration|
* import('@typescript-eslint/types').TSESTree.ClassDeclaration|
* import('@typescript-eslint/types').TSESTree.TSInterfaceDeclaration|
* import('@typescript-eslint/types').TSESTree.TSTypeAliasDeclaration} aliasDeclaration
*/
const checkTypeParams = (aliasDeclaration) => {
/* c8 ignore next -- Guard */
const {params} = aliasDeclaration.typeParameters ?? {params: []};
for (const {name: {name}} of params) {
usedNames.add(name);
}
for (const usedName of usedNames) {
if (!templateNames.includes(usedName)) {
report(`Missing @template ${usedName}`);
}
}
};
const handleTypes = () => {
const nde = /** @type {import('@typescript-eslint/types').TSESTree.Node} */ (
node
);
if (!nde) {
return;
}
switch (nde.type) {
case 'ExportDefaultDeclaration':
switch (nde.declaration?.type) {
case 'ClassDeclaration':
case 'FunctionDeclaration':
case 'TSInterfaceDeclaration':
checkTypeParams(nde.declaration);
break;
}
break;
case 'ExportNamedDeclaration':
switch (nde.declaration?.type) {
case 'ClassDeclaration':
case 'FunctionDeclaration':
case 'TSTypeAliasDeclaration':
case 'TSInterfaceDeclaration':
checkTypeParams(nde.declaration);
break;
}
break;
case 'ClassDeclaration':
case 'FunctionDeclaration':
case 'TSTypeAliasDeclaration':
case 'TSInterfaceDeclaration':
checkTypeParams(nde);
break;
}
};
const usedNameToTag = new Map();
/**
* @param {import('comment-parser').Spec} potentialTag
*/
const checkForUsedTypes = (potentialTag) => {
let parsedType;
try {
parsedType = mode === 'permissive' ?
tryParseType(/** @type {string} */ (potentialTag.type)) :
parseType(/** @type {string} */ (potentialTag.type), mode)
} catch {
return;
}
traverse(parsedType, (nde) => {
const {
type,
value,
} = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde);
if (type === 'JsdocTypeName' && (/^[A-Z]$/).test(value)) {
usedNames.add(value);
if (!usedNameToTag.has(value)) {
usedNameToTag.set(value, potentialTag);
}
}
});
};
/**
* @param {string[]} tagNames
*/
const checkTagsAndTemplates = (tagNames) => {
for (const tagName of tagNames) {
const preferredTagName = /** @type {string} */ (utils.getPreferredTagName({
tagName,
}));
const matchingTags = utils.getTags(preferredTagName);
for (const matchingTag of matchingTags) {
checkForUsedTypes(matchingTag);
}
}
// Could check against whitelist/blacklist
for (const usedName of usedNames) {
if (!templateNames.includes(usedName)) {
report(`Missing @template ${usedName}`, null, usedNameToTag.get(usedName));
}
}
};
const callbackTags = utils.getTags('callback');
const functionTags = utils.getTags('function');
if (callbackTags.length || functionTags.length) {
checkTagsAndTemplates(['param', 'returns']);
return;
}
const typedefTags = utils.getTags('typedef');
if (!typedefTags.length || typedefTags.length >= 2) {
handleTypes();
return;
}
const potentialTypedef = typedefTags[0];
checkForUsedTypes(potentialTypedef);
checkTagsAndTemplates(['property']);
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires template tags for each generic type parameter',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-template.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
requireSeparateTemplates: {
type: 'boolean'
}
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,111 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* We can skip checking for a throws value, in case the documentation is inherited
* or the method is either a constructor or an abstract method.
* @param {import('../iterateJsdoc.js').Utils} utils a reference to the utils which are used to probe if a tag is present or not.
* @returns {boolean} true in case deep checking can be skipped; otherwise false.
*/
const canSkip = (utils) => {
return utils.hasATag([
// inheritdoc implies that all documentation is inherited
// see https://jsdoc.app/tags-inheritdoc.html
//
// Abstract methods are by definition incomplete,
// so it is not necessary to document that they throw an error.
'abstract',
'virtual',
// The designated type can itself document `@throws`
'type',
]) ||
utils.avoidDocs();
};
export default iterateJsdoc(({
report,
utils,
}) => {
// A preflight check. We do not need to run a deep check for abstract
// functions.
if (canSkip(utils)) {
return;
}
const tagName = /** @type {string} */ (utils.getPreferredTagName({
tagName: 'throws',
}));
if (!tagName) {
return;
}
const tags = utils.getTags(tagName);
const iteratingFunction = utils.isIteratingFunction();
// In case the code returns something, we expect a return value in JSDoc.
const [
tag,
] = tags;
const missingThrowsTag = typeof tag === 'undefined' || tag === null;
const shouldReport = () => {
if (!missingThrowsTag) {
if (tag.type.trim() === 'never' && iteratingFunction && utils.hasThrowValue()) {
report(`JSDoc @${tagName} declaration set to "never" but throw value found.`);
}
return false;
}
return iteratingFunction && utils.hasThrowValue();
};
if (shouldReport()) {
report(`Missing JSDoc @${tagName} declaration.`);
}
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires that throw statements are documented.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-throws.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,216 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* We can skip checking for a yield value, in case the documentation is inherited
* or the method has a constructor or abstract tag.
*
* In either of these cases the yield value is optional or not defined.
* @param {import('../iterateJsdoc.js').Utils} utils a reference to the utils which are used to probe if a tag is present or not.
* @returns {boolean} true in case deep checking can be skipped; otherwise false.
*/
const canSkip = (utils) => {
return utils.hasATag([
// inheritdoc implies that all documentation is inherited
// see https://jsdoc.app/tags-inheritdoc.html
//
// Abstract methods are by definition incomplete,
// so it is not an error if it declares a yield value but does not implement it.
'abstract',
'virtual',
// Constructors do not have a yield value
// so we can bail out here, too.
'class',
'constructor',
// Yield (and any `next`) type is specified accompanying the targeted
// @type
'type',
// This seems to imply a class as well
'interface',
]) ||
utils.avoidDocs();
};
/**
* @param {import('../iterateJsdoc.js').Utils} utils
* @param {import('../iterateJsdoc.js').Report} report
* @param {string} tagName
* @returns {[preferredTagName?: string, missingTag?: boolean]}
*/
const checkTagName = (utils, report, tagName) => {
const preferredTagName = /** @type {string} */ (utils.getPreferredTagName({
tagName,
}));
if (!preferredTagName) {
return [];
}
const tags = utils.getTags(preferredTagName);
if (tags.length > 1) {
report(`Found more than one @${preferredTagName} declaration.`);
}
// In case the code yields something, we expect a yields value in JSDoc.
const [
tag,
] = tags;
const missingTag = typeof tag === 'undefined' || tag === null;
return [
preferredTagName, missingTag,
];
};
export default iterateJsdoc(({
report,
utils,
context,
}) => {
const {
next = false,
nextWithGeneratorTag = false,
forceRequireNext = false,
forceRequireYields = false,
withGeneratorTag = true,
} = context.options[0] || {};
// A preflight check. We do not need to run a deep check
// in case the @yield comment is optional or undefined.
if (canSkip(utils)) {
return;
}
const iteratingFunction = utils.isIteratingFunction();
const [
preferredYieldTagName,
missingYieldTag,
] = checkTagName(
utils, report, 'yields',
);
if (preferredYieldTagName) {
const shouldReportYields = () => {
if (!missingYieldTag) {
return false;
}
if (
withGeneratorTag && utils.hasTag('generator') ||
forceRequireYields && iteratingFunction && utils.isGenerator()
) {
return true;
}
return iteratingFunction && utils.isGenerator() && utils.hasYieldValue();
};
if (shouldReportYields()) {
report(`Missing JSDoc @${preferredYieldTagName} declaration.`);
}
}
if (next || nextWithGeneratorTag || forceRequireNext) {
const [
preferredNextTagName,
missingNextTag,
] = checkTagName(
utils, report, 'next',
);
if (!preferredNextTagName) {
return;
}
const shouldReportNext = () => {
if (!missingNextTag) {
return false;
}
if (
nextWithGeneratorTag && utils.hasTag('generator')) {
return true;
}
if (
!next && !forceRequireNext ||
!iteratingFunction ||
!utils.isGenerator()
) {
return false;
}
return forceRequireNext || utils.hasYieldReturnValue();
};
if (shouldReportNext()) {
report(`Missing JSDoc @${preferredNextTagName} declaration.`);
}
}
}, {
contextDefaults: true,
meta: {
docs: {
description: 'Requires yields are documented.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-yields.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
forceRequireNext: {
default: false,
type: 'boolean',
},
forceRequireYields: {
default: false,
type: 'boolean',
},
next: {
default: false,
type: 'boolean',
},
nextWithGeneratorTag: {
default: false,
type: 'boolean',
},
withGeneratorTag: {
default: true,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,208 @@
import iterateJsdoc from '../iterateJsdoc.js';
/**
* @param {import('../iterateJsdoc.js').Utils} utils
* @param {import('../iterateJsdoc.js').Settings} settings
* @returns {boolean}
*/
const canSkip = (utils, settings) => {
const voidingTags = [
// An abstract function is by definition incomplete
// so it is perfectly fine if a yield is documented but
// not present within the function.
// A subclass may inherit the doc and implement the
// missing yield.
'abstract',
'virtual',
// Constructor functions do not have a yield value
// so we can bail here, too.
'class',
'constructor',
// This seems to imply a class as well
'interface',
];
if (settings.mode === 'closure') {
// Structural Interface in GCC terms, equivalent to @interface tag as far as this rule is concerned
voidingTags.push('record');
}
return utils.hasATag(voidingTags) ||
utils.isConstructor() ||
utils.classHasTag('interface') ||
settings.mode === 'closure' && utils.classHasTag('record');
};
/**
* @param {import('../iterateJsdoc.js').Utils} utils
* @param {import('../iterateJsdoc.js').Report} report
* @param {string} tagName
* @returns {[]|[preferredTagName: string, tag: import('comment-parser').Spec]}
*/
const checkTagName = (utils, report, tagName) => {
const preferredTagName = /** @type {string} */ (utils.getPreferredTagName({
tagName,
}));
if (!preferredTagName) {
return [];
}
const tags = utils.getTags(preferredTagName);
if (tags.length === 0) {
return [];
}
if (tags.length > 1) {
report(`Found more than one @${preferredTagName} declaration.`);
return [];
}
return [
preferredTagName, tags[0],
];
};
export default iterateJsdoc(({
context,
report,
settings,
utils,
}) => {
if (canSkip(utils, settings)) {
return;
}
const {
next = false,
checkGeneratorsOnly = false,
} = context.options[0] || {};
const [
preferredYieldTagName,
yieldTag,
] = checkTagName(
utils, report, 'yields',
);
if (preferredYieldTagName) {
const shouldReportYields = () => {
if (
/** @type {import('comment-parser').Spec} */ (
yieldTag
).type.trim() === 'never'
) {
if (utils.hasYieldValue()) {
report(`JSDoc @${preferredYieldTagName} declaration set with "never" but yield expression is present in function.`);
}
return false;
}
if (checkGeneratorsOnly && !utils.isGenerator()) {
return true;
}
return !utils.mayBeUndefinedTypeTag(
/** @type {import('comment-parser').Spec} */
(yieldTag),
) && !utils.hasYieldValue();
};
// In case a yield value is declared in JSDoc, we also expect one in the code.
if (shouldReportYields()) {
report(`JSDoc @${preferredYieldTagName} declaration present but yield expression not available in function.`);
}
}
if (next) {
const [
preferredNextTagName,
nextTag,
] = checkTagName(
utils, report, 'next',
);
if (preferredNextTagName) {
const shouldReportNext = () => {
if (
/** @type {import('comment-parser').Spec} */ (
nextTag
).type.trim() === 'never'
) {
if (utils.hasYieldReturnValue()) {
report(`JSDoc @${preferredNextTagName} declaration set with "never" but yield expression with return value is present in function.`);
}
return false;
}
if (checkGeneratorsOnly && !utils.isGenerator()) {
return true;
}
return !utils.mayBeUndefinedTypeTag(
/** @type {import('comment-parser').Spec} */
(nextTag),
) && !utils.hasYieldReturnValue();
};
if (shouldReportNext()) {
report(`JSDoc @${preferredNextTagName} declaration present but yield expression with return value not available in function.`);
}
}
}
}, {
meta: {
docs: {
description: 'Requires a yield statement in function body if a `@yields` tag is specified in jsdoc comment.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-yields-check.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
checkGeneratorsOnly: {
default: false,
type: 'boolean',
},
contexts: {
items: {
anyOf: [
{
type: 'string',
},
{
additionalProperties: false,
properties: {
comment: {
type: 'string',
},
context: {
type: 'string',
},
},
type: 'object',
},
],
},
type: 'array',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
next: {
default: false,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

557
node_modules/eslint-plugin-jsdoc/src/rules/sortTags.js generated vendored Normal file
View File

@@ -0,0 +1,557 @@
import defaultTagOrder from '../defaultTagOrder.js';
import iterateJsdoc from '../iterateJsdoc.js';
// eslint-disable-next-line complexity -- Temporary
export default iterateJsdoc(({
context,
jsdoc,
utils,
}) => {
const
/**
* @type {{
* linesBetween: import('../iterateJsdoc.js').Integer,
* tagSequence: {
* tags: string[]
* }[],
* alphabetizeExtras: boolean,
* reportTagGroupSpacing: boolean,
* reportIntraTagGroupSpacing: boolean,
* }}
*/ {
linesBetween = 1,
tagSequence = defaultTagOrder,
alphabetizeExtras = false,
reportTagGroupSpacing = true,
reportIntraTagGroupSpacing = true,
} = context.options[0] || {};
const tagList = tagSequence.flatMap((obj) => {
/* typeof obj === 'string' ? obj : */
return obj.tags;
});
const otherPos = tagList.indexOf('-other');
const endPos = otherPos > -1 ? otherPos : tagList.length;
let ongoingCount = 0;
for (const [
idx,
tag,
] of
/**
* @type {(
* import('@es-joy/jsdoccomment').JsdocTagWithInline & {
* originalIndex: import('../iterateJsdoc.js').Integer,
* originalLine: import('../iterateJsdoc.js').Integer,
* }
* )[]}
*/ (jsdoc.tags).entries()) {
tag.originalIndex = idx;
ongoingCount += tag.source.length;
tag.originalLine = ongoingCount;
}
/** @type {import('../iterateJsdoc.js').Integer|undefined} */
let firstChangedTagLine;
/** @type {import('../iterateJsdoc.js').Integer|undefined} */
let firstChangedTagIndex;
/**
* @type {(import('comment-parser').Spec & {
* originalIndex: import('../iterateJsdoc.js').Integer,
* originalLine: import('../iterateJsdoc.js').Integer,
* })[]}
*/
const sortedTags = JSON.parse(JSON.stringify(jsdoc.tags));
sortedTags.sort(({
tag: tagNew,
}, {
originalIndex,
originalLine,
tag: tagOld,
}) => {
// Optimize: Just keep relative positions if the same tag name
if (tagNew === tagOld) {
return 0;
}
const checkOrSetFirstChanged = () => {
if (!firstChangedTagLine || originalLine < firstChangedTagLine) {
firstChangedTagLine = originalLine;
firstChangedTagIndex = originalIndex;
}
};
const newPos = tagList.indexOf(tagNew);
const oldPos = tagList.indexOf(tagOld);
const preferredNewPos = newPos === -1 ? endPos : newPos;
const preferredOldPos = oldPos === -1 ? endPos : oldPos;
if (preferredNewPos < preferredOldPos) {
checkOrSetFirstChanged();
return -1;
}
if (preferredNewPos > preferredOldPos) {
return 1;
}
// preferredNewPos === preferredOldPos
if (
!alphabetizeExtras ||
// Optimize: If tagNew (or tagOld which is the same) was found in the
// priority array, it can maintain its relative position—without need
// of alphabetizing (secondary sorting)
newPos >= 0
) {
return 0;
}
if (tagNew < tagOld) {
checkOrSetFirstChanged();
return -1;
}
// tagNew > tagOld
return 1;
});
if (firstChangedTagLine === undefined) {
// Should be ordered by now
/**
* @type {import('comment-parser').Spec[]}
*/
const lastTagsOfGroup = [];
/**
* @type {[
* import('comment-parser').Spec,
* import('../iterateJsdoc.js').Integer
* ][]}
*/
const badLastTagsOfGroup = [];
/**
* @param {import('comment-parser').Spec} tag
*/
const countTagEmptyLines = (tag) => {
return tag.source.reduce((acc, {
tokens: {
description,
name,
type,
end,
tag: tg,
},
}) => {
const empty = !tg && !type && !name && !description;
// Reset the count so long as there is content
return empty ? acc + Number(empty && !end) : 0;
}, 0);
};
let idx = 0;
for (const {
tags,
} of tagSequence) {
let innerIdx;
/** @type {import('comment-parser').Spec} */
let currentTag;
/** @type {import('comment-parser').Spec|undefined} */
let lastTag;
do {
currentTag = jsdoc.tags[idx];
if (!currentTag) {
idx++;
break;
}
innerIdx = tags.indexOf(currentTag.tag);
if (
innerIdx === -1 &&
// eslint-disable-next-line no-loop-func -- Safe
(!tags.includes('-other') || tagSequence.some(({
tags: tgs,
}) => {
return tgs.includes(currentTag.tag);
}))
) {
idx++;
break;
}
lastTag = currentTag;
idx++;
} while (true);
idx--;
if (lastTag) {
lastTagsOfGroup.push(lastTag);
const ct = countTagEmptyLines(lastTag);
if (
ct !== linesBetween &&
// Use another rule for adding to end (should be of interest outside this rule)
jsdoc.tags[idx]
) {
badLastTagsOfGroup.push([
lastTag, ct,
]);
}
}
}
if (reportTagGroupSpacing && badLastTagsOfGroup.length) {
/**
* @param {import('comment-parser').Spec} tg
* @returns {() => void}
*/
const fixer = (tg) => {
return () => {
// Due to https://github.com/syavorsky/comment-parser/issues/110 ,
// we have to modify `jsdoc.source` rather than just modify tags
// directly
for (const [
currIdx,
{
tokens,
},
] of jsdoc.source.entries()) {
if (tokens.tag !== '@' + tg.tag) {
continue;
}
// Cannot be `tokens.end`, as dropped off last tag, so safe to
// go on
let newIdx = currIdx;
const emptyLine = () => {
return {
number: 0,
source: '',
tokens: utils.seedTokens({
delimiter: '*',
start: jsdoc.source[newIdx - 1].tokens.start,
}),
};
};
let existingEmptyLines = 0;
while (true) {
const nextTokens = jsdoc.source[++newIdx]?.tokens;
/* c8 ignore next 3 -- Guard */
if (!nextTokens) {
return;
}
// Should be no `nextTokens.end` to worry about since ignored
// if not followed by tag
if (nextTokens.tag) {
// Haven't made it to last tag instance yet, so keep looking
if (nextTokens.tag === tokens.tag) {
existingEmptyLines = 0;
continue;
}
const lineDiff = linesBetween - existingEmptyLines;
if (lineDiff > 0) {
const lines = Array.from({
length: lineDiff,
}, () => {
return emptyLine();
});
jsdoc.source.splice(newIdx, 0, ...lines);
} else {
// lineDiff < 0
jsdoc.source.splice(
newIdx + lineDiff,
-lineDiff,
);
}
break;
}
const empty = !nextTokens.type && !nextTokens.name &&
!nextTokens.description;
if (empty) {
existingEmptyLines++;
} else {
// Has content again, so reset empty line count
existingEmptyLines = 0;
}
}
break;
}
for (const [
srcIdx,
src,
] of jsdoc.source.entries()) {
src.number = srcIdx;
}
};
};
for (const [
tg,
] of badLastTagsOfGroup) {
utils.reportJSDoc(
'Tag groups do not have the expected whitespace',
tg,
fixer(tg),
);
}
return;
}
if (!reportIntraTagGroupSpacing) {
return;
}
for (const [
tagIdx,
tag,
] of jsdoc.tags.entries()) {
if (!jsdoc.tags[tagIdx + 1] || lastTagsOfGroup.includes(tag)) {
continue;
}
const ct = countTagEmptyLines(tag);
if (ct) {
// eslint-disable-next-line complexity -- Temporary
const fixer = () => {
let foundFirstTag = false;
/** @type {string|undefined} */
let currentTag;
for (const [
currIdx,
{
tokens: {
description,
name,
type,
end,
tag: tg,
},
},
] of jsdoc.source.entries()) {
if (tg) {
foundFirstTag = true;
currentTag = tg;
}
if (!foundFirstTag) {
continue;
}
if (currentTag && !tg && !type && !name && !description && !end) {
let nextIdx = currIdx;
let ignore = true;
// Even if a tag of the same name as the last tags in a group,
// could still be an earlier tag in that group
// eslint-disable-next-line no-loop-func -- Safe
if (lastTagsOfGroup.some((lastTagOfGroup) => {
return currentTag === '@' + lastTagOfGroup.tag;
})) {
while (true) {
const nextTokens = jsdoc.source[++nextIdx]?.tokens;
if (!nextTokens) {
break;
}
if (!nextTokens.tag) {
continue;
}
// Followed by the same tag name, so not actually last in group,
// and of interest
if (nextTokens.tag === currentTag) {
ignore = false;
}
}
} else {
while (true) {
const nextTokens = jsdoc.source[++nextIdx]?.tokens;
if (!nextTokens || nextTokens.end) {
break;
}
// Not the very last tag, so don't ignore
if (nextTokens.tag) {
ignore = false;
break;
}
}
}
if (!ignore) {
jsdoc.source.splice(currIdx, 1);
for (const [
srcIdx,
src,
] of jsdoc.source.entries()) {
src.number = srcIdx;
}
}
}
}
};
utils.reportJSDoc(
'Intra-group tags have unexpected whitespace',
tag,
fixer,
);
}
}
return;
}
const firstLine = utils.getFirstLine();
const fix = () => {
const itemsToMoveRange = [
...Array.from({
length: jsdoc.tags.length -
/** @type {import('../iterateJsdoc.js').Integer} */ (
firstChangedTagIndex
),
}).keys(),
];
const unchangedPriorTagDescriptions = jsdoc.tags.slice(
0,
firstChangedTagIndex,
).reduce((ct, {
source,
}) => {
return ct + source.length - 1;
}, 0);
// This offset includes not only the offset from where the first tag
// must begin, and the additional offset of where the first changed
// tag begins, but it must also account for prior descriptions
const initialOffset = /** @type {import('../iterateJsdoc.js').Integer} */ (
firstLine
) + /** @type {import('../iterateJsdoc.js').Integer} */ (firstChangedTagIndex) +
// May be the first tag, so don't try finding a prior one if so
unchangedPriorTagDescriptions;
// Use `firstChangedTagLine` for line number to begin reporting/splicing
for (const idx of itemsToMoveRange) {
utils.removeTag(
idx +
/** @type {import('../iterateJsdoc.js').Integer} */ (
firstChangedTagIndex
),
);
}
const changedTags = sortedTags.slice(firstChangedTagIndex);
let extraTagCount = 0;
for (const idx of itemsToMoveRange) {
const changedTag = changedTags[idx];
utils.addTag(
changedTag.tag,
extraTagCount + initialOffset + idx,
{
...changedTag.source[0].tokens,
// `comment-parser` puts the `end` within the `tags` section, so
// avoid adding another to jsdoc.source
end: '',
},
);
for (const {
tokens,
} of changedTag.source.slice(1)) {
if (!tokens.end) {
utils.addLine(
extraTagCount + initialOffset + idx + 1,
{
...tokens,
end: '',
},
);
extraTagCount++;
}
}
}
};
utils.reportJSDoc(
`Tags are not in the prescribed order: ${
tagList.join(', ') || '(alphabetical)'
}`,
jsdoc.tags[/** @type {import('../iterateJsdoc.js').Integer} */ (
firstChangedTagIndex
)],
fix,
true,
);
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Sorts tags by a specified sequence according to tag name.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/sort-tags.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
alphabetizeExtras: {
type: 'boolean',
},
linesBetween: {
type: 'integer',
},
reportIntraTagGroupSpacing: {
type: 'boolean',
},
reportTagGroupSpacing: {
type: 'boolean',
},
tagSequence: {
items: {
properties: {
tags: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

359
node_modules/eslint-plugin-jsdoc/src/rules/tagLines.js generated vendored Normal file
View File

@@ -0,0 +1,359 @@
import iterateJsdoc from '../iterateJsdoc.js';
export default iterateJsdoc(({
context,
jsdoc,
utils,
}) => {
const [
alwaysNever = 'never',
{
count = 1,
endLines = 0,
startLines = 0,
applyToEndTag = true,
tags = {},
} = {},
] = context.options;
// eslint-disable-next-line complexity -- Temporary
jsdoc.tags.some((tg, tagIdx) => {
let lastTag;
/**
* @type {null|import('../iterateJsdoc.js').Integer}
*/
let lastEmpty = null;
/**
* @type {null|import('../iterateJsdoc.js').Integer}
*/
let reportIndex = null;
let emptyLinesCount = 0;
for (const [
idx,
{
tokens: {
tag,
name,
type,
description,
end,
},
},
] of tg.source.entries()) {
// May be text after a line break within a tag description
if (description) {
reportIndex = null;
}
if (lastTag && [
'any', 'always',
].includes(tags[lastTag.slice(1)]?.lines)) {
continue;
}
const empty = !tag && !name && !type && !description;
if (
empty && !end &&
(alwaysNever === 'never' ||
lastTag && tags[lastTag.slice(1)]?.lines === 'never'
)
) {
reportIndex = idx;
continue;
}
if (!end) {
if (empty) {
emptyLinesCount++;
} else {
emptyLinesCount = 0;
}
lastEmpty = empty ? idx : null;
}
lastTag = tag;
}
if (
typeof endLines === 'number' &&
lastEmpty !== null && tagIdx === jsdoc.tags.length - 1
) {
const lineDiff = endLines - emptyLinesCount;
if (lineDiff < 0) {
const fixer = () => {
utils.removeTag(tagIdx, {
tagSourceOffset: /** @type {import('../iterateJsdoc.js').Integer} */ (
lastEmpty
) + lineDiff + 1,
});
};
utils.reportJSDoc(
`Expected ${endLines} trailing lines`,
{
line: tg.source[lastEmpty].number + lineDiff + 1,
},
fixer,
);
} else if (lineDiff > 0) {
const fixer = () => {
utils.addLines(
tagIdx,
/** @type {import('../iterateJsdoc.js').Integer} */ (lastEmpty),
endLines - emptyLinesCount,
);
};
utils.reportJSDoc(
`Expected ${endLines} trailing lines`,
{
line: tg.source[lastEmpty].number,
},
fixer,
);
}
return true;
}
if (reportIndex !== null) {
const fixer = () => {
utils.removeTag(tagIdx, {
tagSourceOffset: /** @type {import('../iterateJsdoc.js').Integer} */ (
reportIndex
),
});
};
utils.reportJSDoc(
'Expected no lines between tags',
{
line: tg.source[0].number + 1,
},
fixer,
);
return true;
}
return false;
});
(applyToEndTag ? jsdoc.tags : jsdoc.tags.slice(0, -1)).some((tg, tagIdx) => {
/**
* @type {{
* idx: import('../iterateJsdoc.js').Integer,
* number: import('../iterateJsdoc.js').Integer
* }[]}
*/
const lines = [];
let currentTag;
let tagSourceIdx = 0;
for (const [
idx,
{
number,
tokens: {
tag,
name,
type,
description,
end,
},
},
] of tg.source.entries()) {
if (description) {
lines.splice(0, lines.length);
tagSourceIdx = idx;
}
if (tag) {
currentTag = tag;
}
if (!tag && !name && !type && !description && !end) {
lines.push({
idx,
number,
});
}
}
const currentTg = currentTag && tags[currentTag.slice(1)];
const tagCount = currentTg?.count;
const defaultAlways = alwaysNever === 'always' && currentTg?.lines !== 'never' &&
currentTg?.lines !== 'any' && lines.length < count;
let overrideAlways;
let fixCount = count;
if (!defaultAlways) {
fixCount = typeof tagCount === 'number' ? tagCount : count;
overrideAlways = currentTg?.lines === 'always' &&
lines.length < fixCount;
}
if (defaultAlways || overrideAlways) {
const fixer = () => {
utils.addLines(tagIdx, lines[lines.length - 1]?.idx || tagSourceIdx + 1, fixCount - lines.length);
};
const line = lines[lines.length - 1]?.number || tg.source[tagSourceIdx].number;
utils.reportJSDoc(
`Expected ${fixCount} line${fixCount === 1 ? '' : 's'} between tags but found ${lines.length}`,
{
line,
},
fixer,
);
return true;
}
return false;
});
if (typeof startLines === 'number') {
if (!jsdoc.tags.length) {
return;
}
const {
description,
lastDescriptionLine,
} = utils.getDescription();
if (!(/\S/u).test(description)) {
return;
}
const trailingLines = description.match(/\n+$/u)?.[0]?.length;
const trailingDiff = (trailingLines ?? 0) - startLines;
if (trailingDiff > 0) {
utils.reportJSDoc(
`Expected only ${startLines} line after block description`,
{
line: lastDescriptionLine - trailingDiff,
},
() => {
utils.setBlockDescription((info, seedTokens, descLines) => {
return descLines.slice(0, -trailingDiff).map((desc) => {
return {
number: 0,
source: '',
tokens: seedTokens({
...info,
description: desc,
postDelimiter: desc.trim() ? info.postDelimiter : '',
}),
};
});
});
},
);
} else if (trailingDiff < 0) {
utils.reportJSDoc(
`Expected ${startLines} lines after block description`,
{
line: lastDescriptionLine,
},
() => {
utils.setBlockDescription((info, seedTokens, descLines) => {
return [
...descLines,
...Array.from({
length: -trailingDiff,
}, () => {
return '';
}),
].map((desc) => {
return {
number: 0,
source: '',
tokens: seedTokens({
...info,
description: desc,
postDelimiter: desc.trim() ? info.postDelimiter : '',
}),
};
});
});
},
);
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Enforces lines (or no lines) between tags.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/tag-lines.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
enum: [
'always', 'any', 'never',
],
type: 'string',
},
{
additionalProperties: false,
properties: {
applyToEndTag: {
type: 'boolean',
},
count: {
type: 'integer',
},
endLines: {
anyOf: [
{
type: 'integer',
},
{
type: 'null',
},
],
},
startLines: {
anyOf: [
{
type: 'integer',
},
{
type: 'null',
},
],
},
tags: {
patternProperties: {
'.*': {
additionalProperties: false,
properties: {
count: {
type: 'integer',
},
lines: {
enum: [
'always', 'never', 'any',
],
type: 'string',
},
},
},
},
type: 'object',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,146 @@
import iterateJsdoc from '../iterateJsdoc.js';
// We could disallow raw gt, quot, and apos, but allow for parity; but we do
// not allow hex or decimal character references
const htmlRegex = /(<|&(?!(?:amp|lt|gt|quot|apos);))(?=\S)/u;
const markdownRegex = /(?<!\\)(`+)([^`]+)\1(?!`)/u;
/**
* @param {string} desc
* @returns {string}
*/
const htmlReplacer = (desc) => {
return desc.replaceAll(new RegExp(htmlRegex, 'gu'), (_) => {
if (_ === '<') {
return '&lt;';
}
return '&amp;';
});
};
/**
* @param {string} desc
* @returns {string}
*/
const markdownReplacer = (desc) => {
return desc.replaceAll(new RegExp(markdownRegex, 'gu'), (_, backticks, encapsed) => {
const bookend = '`'.repeat(backticks.length);
return `\\${bookend}${encapsed}${bookend}`;
});
};
export default iterateJsdoc(({
context,
jsdoc,
utils,
}) => {
const {
escapeHTML,
escapeMarkdown,
} = context.options[0] || {};
if (!escapeHTML && !escapeMarkdown) {
context.report({
loc: {
end: {
column: 1,
line: 1,
},
start: {
column: 1,
line: 1,
},
},
message: 'You must include either `escapeHTML` or `escapeMarkdown`',
});
return;
}
const {
descriptions,
} = utils.getDescription();
if (escapeHTML) {
if (descriptions.some((desc) => {
return htmlRegex.test(desc);
})) {
const line = utils.setDescriptionLines(htmlRegex, htmlReplacer);
utils.reportJSDoc('You have unescaped HTML characters < or &', {
line,
}, () => {}, true);
return;
}
for (const tag of jsdoc.tags) {
if (/** @type {string[]} */ (
utils.getTagDescription(tag, true)
).some((desc) => {
return htmlRegex.test(desc);
})) {
const line = utils.setTagDescription(tag, htmlRegex, htmlReplacer) +
tag.source[0].number;
utils.reportJSDoc('You have unescaped HTML characters < or & in a tag', {
line,
}, () => {}, true);
}
}
return;
}
if (descriptions.some((desc) => {
return markdownRegex.test(desc);
})) {
const line = utils.setDescriptionLines(markdownRegex, markdownReplacer);
utils.reportJSDoc('You have unescaped Markdown backtick sequences', {
line,
}, () => {}, true);
return;
}
for (const tag of jsdoc.tags) {
if (/** @type {string[]} */ (
utils.getTagDescription(tag, true)
).some((desc) => {
return markdownRegex.test(desc);
})) {
const line = utils.setTagDescription(
tag, markdownRegex, markdownReplacer,
) + tag.source[0].number;
utils.reportJSDoc(
'You have unescaped Markdown backtick sequences in a tag',
{
line,
},
() => {},
true,
);
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: '',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/text-escaping.md#repos-sticky-header',
},
fixable: 'code',
schema: [
{
additionalProperties: false,
properties: {
// Option properties here (or remove the object)
escapeHTML: {
type: 'boolean',
},
escapeMarkdown: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

View File

@@ -0,0 +1,384 @@
import iterateJsdoc from '../iterateJsdoc.js';
import {
parse,
traverse,
tryParse,
} from '@es-joy/jsdoccomment';
const inlineTags = new Set([
'link', 'linkcode', 'linkplain',
'tutorial',
]);
const jsdocTypePrattKeywords = new Set([
'typeof',
'readonly',
'import',
'is',
]);
const asExpression = /as\s+/u;
const suppressTypes = new Set([
// https://github.com/google/closure-compiler/wiki/@suppress-annotations
// https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/parsing/ParserConfig.properties#L154
'accessControls',
'checkDebuggerStatement',
'checkPrototypalTypes',
'checkRegExp',
'checkTypes',
'checkVars',
'closureDepMethodUsageChecks',
'const',
'constantProperty',
'deprecated',
'duplicate',
'es5Strict',
'externsValidation',
'extraProvide',
'extraRequire',
'globalThis',
'invalidCasts',
'lateProvide',
'legacyGoogScopeRequire',
'lintChecks',
'messageConventions',
'misplacedTypeAnnotation',
'missingOverride',
'missingPolyfill',
'missingProperties',
'missingProvide',
'missingRequire',
'missingSourcesWarnings',
'moduleLoad',
'nonStandardJsDocs',
'partialAlias',
'polymer',
'reportUnknownTypes',
'strictMissingProperties',
'strictModuleDepCheck',
'strictPrimitiveOperators',
'suspiciousCode',
// Not documented in enum
'switch',
'transitionalSuspiciousCodeWarnings',
'undefinedNames',
'undefinedVars',
'underscore',
'unknownDefines',
'untranspilableFeatures',
'unusedLocalVariables',
'unusedPrivateMembers',
'useOfGoogProvide',
'uselessCode',
'visibility',
'with',
]);
/**
* @param {string} path
* @returns {boolean}
*/
const tryParsePathIgnoreError = (path) => {
try {
tryParse(path);
return true;
} catch {
// Keep the original error for including the whole type
}
return false;
};
// eslint-disable-next-line complexity
export default iterateJsdoc(({
jsdoc,
report,
utils,
context,
settings,
}) => {
const {
allowEmptyNamepaths = false,
} = context.options[0] || {};
const {
mode,
} = settings;
for (const tag of jsdoc.tags) {
/**
* @param {string} namepath
* @param {string} [tagName]
* @returns {boolean}
*/
const validNamepathParsing = function (namepath, tagName) {
if (
tryParsePathIgnoreError(namepath) ||
jsdocTypePrattKeywords.has(namepath)
) {
return true;
}
let handled = false;
if (tagName) {
switch (tagName) {
case 'requires':
case 'module': {
if (!namepath.startsWith('module:')) {
handled = tryParsePathIgnoreError(`module:${namepath}`);
}
break;
}
case 'memberof': case 'memberof!': {
const endChar = namepath.slice(-1);
if ([
'#', '.', '~',
].includes(endChar)) {
handled = tryParsePathIgnoreError(namepath.slice(0, -1));
}
break;
}
case 'borrows': {
const startChar = namepath.charAt(0);
if ([
'#', '.', '~',
].includes(startChar)) {
handled = tryParsePathIgnoreError(namepath.slice(1));
}
}
}
}
if (!handled) {
report(`Syntax error in namepath: ${namepath}`, null, tag);
return false;
}
return true;
};
/**
* @param {string} type
* @returns {boolean}
*/
const validTypeParsing = function (type) {
let parsedTypes;
try {
if (mode === 'permissive') {
parsedTypes = tryParse(type);
} else {
parsedTypes = parse(type, mode);
}
} catch {
report(`Syntax error in type: ${type}`, null, tag);
return false;
}
if (mode === 'closure' || mode === 'typescript') {
traverse(parsedTypes, (node) => {
const {
type: typ,
} = node;
if (
(typ === 'JsdocTypeObjectField' || typ === 'JsdocTypeKeyValue') &&
node.right?.type === 'JsdocTypeNullable' &&
node.right?.meta?.position === 'suffix'
) {
report(`Syntax error in type: ${node.right.type}`, null, tag);
}
});
}
return true;
};
if (tag.problems.length) {
const msg = tag.problems.reduce((str, {
message,
}) => {
return str + '; ' + message;
}, '').slice(2);
report(`Invalid name: ${msg}`, null, tag);
continue;
}
if (tag.tag === 'import') {
// A named import will look like a type, but not be valid; we also don't
// need to check the name/namepath
continue;
}
if (tag.tag === 'borrows') {
const thisNamepath = /** @type {string} */ (
utils.getTagDescription(tag)
).replace(asExpression, '')
.trim();
if (!asExpression.test(/** @type {string} */ (
utils.getTagDescription(tag)
)) || !thisNamepath) {
report(`@borrows must have an "as" expression. Found "${utils.getTagDescription(tag)}"`, null, tag);
continue;
}
if (validNamepathParsing(thisNamepath, 'borrows')) {
const thatNamepath = tag.name;
validNamepathParsing(thatNamepath);
}
continue;
}
if (tag.tag === 'suppress' && mode === 'closure') {
let parsedTypes;
try {
parsedTypes = tryParse(tag.type);
} catch {
// Ignore
}
if (parsedTypes) {
traverse(parsedTypes, (node) => {
let type;
if ('value' in node && typeof node.value === 'string') {
type = node.value;
}
if (type !== undefined && !suppressTypes.has(type)) {
report(`Syntax error in suppress type: ${type}`, null, tag);
}
});
}
}
const otherModeMaps = /** @type {import('../jsdocUtils.js').ParserMode[]} */ ([
'jsdoc', 'typescript', 'closure', 'permissive',
]).filter(
(mde) => {
return mde !== mode;
},
).map((mde) => {
return utils.getTagStructureForMode(mde);
});
const tagMightHaveNamePosition = utils.tagMightHaveNamePosition(tag.tag, otherModeMaps);
if (tagMightHaveNamePosition !== true && tag.name) {
const modeInfo = tagMightHaveNamePosition === false ? '' : ` in "${mode}" mode`;
report(`@${tag.tag} should not have a name${modeInfo}.`, null, tag);
continue;
}
const mightHaveTypePosition = utils.tagMightHaveTypePosition(tag.tag, otherModeMaps);
if (mightHaveTypePosition !== true && tag.type) {
const modeInfo = mightHaveTypePosition === false ? '' : ` in "${mode}" mode`;
report(`@${tag.tag} should not have a bracketed type${modeInfo}.`, null, tag);
continue;
}
// REQUIRED NAME
const tagMustHaveNamePosition = utils.tagMustHaveNamePosition(tag.tag, otherModeMaps);
// Don't handle `@param` here though it does require name as handled by
// `require-param-name` (`@property` would similarly seem to require one,
// but is handled by `require-property-name`)
if (tagMustHaveNamePosition !== false && !tag.name && !allowEmptyNamepaths && ![
'param', 'arg', 'argument',
'property', 'prop',
].includes(tag.tag) &&
(tag.tag !== 'see' || !utils.getTagDescription(tag).includes('{@link'))
) {
const modeInfo = tagMustHaveNamePosition === true ? '' : ` in "${mode}" mode`;
report(`Tag @${tag.tag} must have a name/namepath${modeInfo}.`, null, tag);
continue;
}
// REQUIRED TYPE
const mustHaveTypePosition = utils.tagMustHaveTypePosition(tag.tag, otherModeMaps);
if (mustHaveTypePosition !== false && !tag.type) {
const modeInfo = mustHaveTypePosition === true ? '' : ` in "${mode}" mode`;
report(`Tag @${tag.tag} must have a type${modeInfo}.`, null, tag);
continue;
}
// REQUIRED TYPE OR NAME/NAMEPATH
const tagMissingRequiredTypeOrNamepath = utils.tagMissingRequiredTypeOrNamepath(tag, otherModeMaps);
if (tagMissingRequiredTypeOrNamepath !== false && !allowEmptyNamepaths) {
const modeInfo = tagMissingRequiredTypeOrNamepath === true ? '' : ` in "${mode}" mode`;
report(`Tag @${tag.tag} must have either a type or namepath${modeInfo}.`, null, tag);
continue;
}
// VALID TYPE
const hasTypePosition = mightHaveTypePosition === true && Boolean(tag.type);
if (hasTypePosition) {
validTypeParsing(tag.type);
}
// VALID NAME/NAMEPATH
const hasNameOrNamepathPosition = (
tagMustHaveNamePosition !== false ||
utils.tagMightHaveNamepath(tag.tag)
) && Boolean(tag.name);
if (hasNameOrNamepathPosition) {
if (mode !== 'jsdoc' && tag.tag === 'template') {
for (const namepath of utils.parseClosureTemplateTag(tag)) {
validNamepathParsing(namepath);
}
} else {
validNamepathParsing(tag.name, tag.tag);
}
}
for (const inlineTag of tag.inlineTags) {
if (inlineTags.has(inlineTag.tag) && !inlineTag.text && !inlineTag.namepathOrURL) {
report(`Inline tag "${inlineTag.tag}" missing content`, null, tag);
}
}
}
for (const inlineTag of jsdoc.inlineTags) {
if (inlineTags.has(inlineTag.tag) && !inlineTag.text && !inlineTag.namepathOrURL) {
report(`Inline tag "${inlineTag.tag}" missing content`);
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Requires all types to be valid JSDoc or Closure compiler types without syntax errors.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/valid-types.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
allowEmptyNamepaths: {
default: false,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});

238
node_modules/eslint-plugin-jsdoc/src/tagNames.js generated vendored Normal file
View File

@@ -0,0 +1,238 @@
/**
* @typedef {{
* [key: string]: string[]
* }} AliasedTags
*/
/**
* @type {AliasedTags}
*/
const jsdocTagsUndocumented = {
// Undocumented but present; see
// https://github.com/jsdoc/jsdoc/issues/1283#issuecomment-516816802
// https://github.com/jsdoc/jsdoc/blob/master/packages/jsdoc/lib/jsdoc/tag/dictionary/definitions.js#L594
modifies: [],
};
/**
* @type {AliasedTags}
*/
const jsdocTags = {
...jsdocTagsUndocumented,
abstract: [
'virtual',
],
access: [],
alias: [],
async: [],
augments: [
'extends',
],
author: [],
borrows: [],
callback: [],
class: [
'constructor',
],
classdesc: [],
constant: [
'const',
],
constructs: [],
copyright: [],
default: [
'defaultvalue',
],
deprecated: [],
description: [
'desc',
],
enum: [],
event: [],
example: [],
exports: [],
external: [
'host',
],
file: [
'fileoverview',
'overview',
],
fires: [
'emits',
],
function: [
'func',
'method',
],
generator: [],
global: [],
hideconstructor: [],
ignore: [],
implements: [],
inheritdoc: [],
// Allowing casing distinct from jsdoc `definitions.js` (required in Closure)
inheritDoc: [],
inner: [],
instance: [],
interface: [],
kind: [],
lends: [],
license: [],
listens: [],
member: [
'var',
],
memberof: [],
'memberof!': [],
mixes: [],
mixin: [],
module: [],
name: [],
namespace: [],
override: [],
package: [],
param: [
'arg',
'argument',
],
private: [],
property: [
'prop',
],
protected: [],
public: [],
readonly: [],
requires: [],
returns: [
'return',
],
see: [],
since: [],
static: [],
summary: [],
this: [],
throws: [
'exception',
],
todo: [],
tutorial: [],
type: [],
typedef: [],
variation: [],
version: [],
yields: [
'yield',
],
};
/**
* @type {AliasedTags}
*/
const typeScriptTags = {
...jsdocTags,
// https://github.com/microsoft/TypeScript/issues/22160
// https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-import-tag
import: [],
// https://www.typescriptlang.org/tsconfig/#stripInternal
internal: [],
// https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#overload-support-in-jsdoc
overload: [],
// https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#satisfies-support-in-jsdoc
satisfies: [],
// `@template` is also in TypeScript per:
// https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc
template: [],
};
/**
* @type {AliasedTags}
*/
const undocumentedClosureTags = {
// These are in Closure source but not in jsdoc source nor in the Closure
// docs: https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/parsing/Annotation.java
closurePrimitive: [],
customElement: [],
expose: [],
hidden: [],
idGenerator: [],
meaning: [],
mixinClass: [],
mixinFunction: [],
ngInject: [],
owner: [],
typeSummary: [],
wizaction: [],
};
const {
/* eslint-disable no-unused-vars */
inheritdoc,
internal,
overload,
satisfies,
// Will be inverted to prefer `return`
returns,
/* eslint-enable no-unused-vars */
...typeScriptTagsInClosure
} = typeScriptTags;
/**
* @type {AliasedTags}
*/
const closureTags = {
...typeScriptTagsInClosure,
...undocumentedClosureTags,
// From https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler
// These are all recognized in https://github.com/jsdoc/jsdoc/blob/master/packages/jsdoc/lib/jsdoc/tag/dictionary/definitions.js
// except for the experimental `noinline` and the casing differences noted below
// Defined as a synonym of `const` in jsdoc `definitions.js`
define: [],
dict: [],
export: [],
externs: [],
final: [],
// With casing distinct from jsdoc `definitions.js`
implicitCast: [],
noalias: [],
nocollapse: [],
nocompile: [],
noinline: [],
nosideeffects: [],
polymer: [],
polymerBehavior: [],
preserve: [],
// Defined as a synonym of `interface` in jsdoc `definitions.js`
record: [],
return: [
'returns',
],
struct: [],
suppress: [],
unrestricted: [],
};
export {
closureTags,
jsdocTags,
typeScriptTags,
};

View File

@@ -0,0 +1,549 @@
/**
* @typedef {import('estree').Node|
* import('@typescript-eslint/types').TSESTree.Node} ESTreeOrTypeScriptNode
*/
/**
* Checks if a node is a promise but has no resolve value or an empty value.
* An `undefined` resolve does not count.
* @param {ESTreeOrTypeScriptNode|undefined|null} node
* @returns {boolean|undefined|null}
*/
const isNewPromiseExpression = (node) => {
return node && node.type === 'NewExpression' && node.callee.type === 'Identifier' &&
node.callee.name === 'Promise';
};
/**
* @param {ESTreeOrTypeScriptNode|null|undefined} node
* @returns {boolean}
*/
const isVoidPromise = (node) => {
return /** @type {import('@typescript-eslint/types').TSESTree.TSTypeReference} */ (node)?.typeArguments?.params?.[0]?.type === 'TSVoidKeyword'
/* c8 ignore next */
|| /** @type {import('@typescript-eslint/types').TSESTree.TSTypeReference} */ (node)?.typeParameters?.params?.[0]?.type === 'TSVoidKeyword';
};
const undefinedKeywords = new Set([
'TSVoidKeyword', 'TSUndefinedKeyword', 'TSNeverKeyword',
]);
/**
* Checks if a node has a return statement. Void return does not count.
* @param {ESTreeOrTypeScriptNode|undefined|null} node
* @param {boolean} [throwOnNullReturn]
* @param {PromiseFilter} [promFilter]
* @returns {boolean|undefined}
*/
// eslint-disable-next-line complexity
const hasReturnValue = (node, throwOnNullReturn, promFilter) => {
if (!node) {
return false;
}
switch (node.type) {
case 'TSDeclareFunction':
case 'TSFunctionType':
case 'TSMethodSignature': {
const type = node?.returnType?.typeAnnotation?.type;
return type && !undefinedKeywords.has(type);
}
case 'MethodDefinition':
return hasReturnValue(node.value, throwOnNullReturn, promFilter);
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression': {
return 'expression' in node && node.expression && (!isNewPromiseExpression(
node.body,
) || !isVoidPromise(node.body)) ||
hasReturnValue(node.body, throwOnNullReturn, promFilter);
}
case 'BlockStatement': {
return node.body.some((bodyNode) => {
return bodyNode.type !== 'FunctionDeclaration' && hasReturnValue(bodyNode, throwOnNullReturn, promFilter);
});
}
case 'LabeledStatement':
case 'WhileStatement':
case 'DoWhileStatement':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'WithStatement': {
return hasReturnValue(node.body, throwOnNullReturn, promFilter);
}
case 'IfStatement': {
return hasReturnValue(node.consequent, throwOnNullReturn, promFilter) ||
hasReturnValue(node.alternate, throwOnNullReturn, promFilter);
}
case 'TryStatement': {
return hasReturnValue(node.block, throwOnNullReturn, promFilter) ||
hasReturnValue(node.handler && node.handler.body, throwOnNullReturn, promFilter) ||
hasReturnValue(node.finalizer, throwOnNullReturn, promFilter);
}
case 'SwitchStatement': {
return node.cases.some(
(someCase) => {
return someCase.consequent.some((nde) => {
return hasReturnValue(nde, throwOnNullReturn, promFilter);
});
},
);
}
case 'ReturnStatement': {
// void return does not count.
if (node.argument === null) {
if (throwOnNullReturn) {
throw new Error('Null return');
}
return false;
}
if (promFilter && isNewPromiseExpression(node.argument)) {
// Let caller decide how to filter, but this is, at the least,
// a return of sorts and truthy
return promFilter(node.argument);
}
return true;
}
default: {
return false;
}
}
};
/**
* Checks if a node has a return statement. Void return does not count.
* @param {ESTreeOrTypeScriptNode|null|undefined} node
* @param {PromiseFilter} promFilter
* @returns {undefined|boolean|ESTreeOrTypeScriptNode}
*/
// eslint-disable-next-line complexity
const allBrancheshaveReturnValues = (node, promFilter) => {
if (!node) {
return false;
}
switch (node.type) {
case 'TSDeclareFunction':
case 'TSFunctionType':
case 'TSMethodSignature': {
const type = node?.returnType?.typeAnnotation?.type;
return type && !undefinedKeywords.has(type);
}
// case 'MethodDefinition':
// return allBrancheshaveReturnValues(node.value, promFilter);
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression': {
return 'expression' in node && node.expression && (!isNewPromiseExpression(node.body) || !isVoidPromise(node.body)) ||
allBrancheshaveReturnValues(node.body, promFilter) ||
/** @type {import('@typescript-eslint/types').TSESTree.BlockStatement} */
(node.body).body.some((nde) => {
return nde.type === 'ReturnStatement';
});
}
case 'BlockStatement': {
const lastBodyNode = node.body.slice(-1)[0];
return allBrancheshaveReturnValues(lastBodyNode, promFilter);
}
case 'WhileStatement':
case 'DoWhileStatement':
if (
/**
* @type {import('@typescript-eslint/types').TSESTree.Literal}
*/
(node.test).value === true
) {
// If this is an infinite loop, we assume only one branch
// is needed to provide a return
return hasReturnValue(node.body, false, promFilter);
}
// Fallthrough
case 'LabeledStatement':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'WithStatement': {
return allBrancheshaveReturnValues(node.body, promFilter);
}
case 'IfStatement': {
return allBrancheshaveReturnValues(node.consequent, promFilter) &&
allBrancheshaveReturnValues(node.alternate, promFilter);
}
case 'TryStatement': {
// If `finally` returns, all return
return node.finalizer && allBrancheshaveReturnValues(node.finalizer, promFilter) ||
// Return in `try`/`catch` may still occur despite `finally`
allBrancheshaveReturnValues(node.block, promFilter) &&
(!node.handler ||
allBrancheshaveReturnValues(node.handler && node.handler.body, promFilter)) &&
(!node.finalizer || (() => {
try {
hasReturnValue(node.finalizer, true, promFilter);
} catch (error) {
if (/** @type {Error} */ (error).message === 'Null return') {
return false;
}
/* c8 ignore next 2 */
throw error;
}
// As long as not an explicit empty return, then return true
return true;
})());
}
case 'SwitchStatement': {
return /** @type {import('@typescript-eslint/types').TSESTree.SwitchStatement} */ (node).cases.every(
(someCase) => {
return !someCase.consequent.some((consNode) => {
return consNode.type === 'BreakStatement' ||
consNode.type === 'ReturnStatement' && consNode.argument === null;
});
},
);
}
case 'ThrowStatement': {
return true;
}
case 'ReturnStatement': {
// void return does not count.
if (node.argument === null) {
return false;
}
if (promFilter && isNewPromiseExpression(node.argument)) {
// Let caller decide how to filter, but this is, at the least,
// a return of sorts and truthy
return promFilter(node.argument);
}
return true;
}
default: {
return false;
}
}
};
/**
* @callback PromiseFilter
* @param {ESTreeOrTypeScriptNode|undefined} node
* @returns {boolean}
*/
/**
* Avoids further checking child nodes if a nested function shadows the
* resolver, but otherwise, if name is used (by call or passed in as an
* argument to another function), will be considered as non-empty.
*
* This could check for redeclaration of the resolver, but as such is
* unlikely, we avoid the performance cost of checking everywhere for
* (re)declarations or assignments.
* @param {import('@typescript-eslint/types').TSESTree.Node|null|undefined} node
* @param {string} resolverName
* @returns {boolean}
*/
// eslint-disable-next-line complexity
const hasNonEmptyResolverCall = (node, resolverName) => {
if (!node) {
return false;
}
// Arrow function without block
switch (node.type) {
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'OptionalCallExpression':
case 'CallExpression':
return /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
node.callee
).name === resolverName && (
// Implicit or explicit undefined
node.arguments.length > 1 || node.arguments[0] !== undefined
) ||
node.arguments.some((nde) => {
// Being passed in to another function (which might invoke it)
return nde.type === 'Identifier' && nde.name === resolverName ||
// Handle nested items
hasNonEmptyResolverCall(nde, resolverName);
});
case 'ChainExpression':
case 'Decorator':
case 'ExpressionStatement':
return hasNonEmptyResolverCall(node.expression, resolverName);
case 'ClassBody':
case 'BlockStatement':
return node.body.some((bodyNode) => {
return hasNonEmptyResolverCall(bodyNode, resolverName);
});
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression': {
// Shadowing
if (/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
node.params[0]
)?.name === resolverName) {
return false;
}
return hasNonEmptyResolverCall(node.body, resolverName);
}
case 'LabeledStatement':
case 'WhileStatement':
case 'DoWhileStatement':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'WithStatement': {
return hasNonEmptyResolverCall(node.body, resolverName);
}
case 'ConditionalExpression':
case 'IfStatement': {
return hasNonEmptyResolverCall(node.test, resolverName) ||
hasNonEmptyResolverCall(node.consequent, resolverName) ||
hasNonEmptyResolverCall(node.alternate, resolverName);
}
case 'TryStatement': {
return hasNonEmptyResolverCall(node.block, resolverName) ||
hasNonEmptyResolverCall(node.handler && node.handler.body, resolverName) ||
hasNonEmptyResolverCall(node.finalizer, resolverName);
}
case 'SwitchStatement': {
return node.cases.some(
(someCase) => {
return someCase.consequent.some((nde) => {
return hasNonEmptyResolverCall(nde, resolverName);
});
},
);
}
case 'ArrayPattern':
case 'ArrayExpression':
return node.elements.some((element) => {
return hasNonEmptyResolverCall(element, resolverName);
});
case 'AssignmentPattern':
return hasNonEmptyResolverCall(node.right, resolverName);
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression': {
return hasNonEmptyResolverCall(node.left, resolverName) ||
hasNonEmptyResolverCall(node.right, resolverName);
}
// Comma
case 'SequenceExpression':
case 'TemplateLiteral':
return node.expressions.some((subExpression) => {
return hasNonEmptyResolverCall(subExpression, resolverName);
});
case 'ObjectPattern':
case 'ObjectExpression':
return node.properties.some((property) => {
return hasNonEmptyResolverCall(property, resolverName);
});
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ClassMethod':
case 'MethodDefinition':
return node.decorators && node.decorators.some((decorator) => {
return hasNonEmptyResolverCall(decorator, resolverName);
}) ||
node.computed && hasNonEmptyResolverCall(node.key, resolverName) ||
hasNonEmptyResolverCall(node.value, resolverName);
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ObjectProperty':
/* eslint-disable no-fallthrough */
/* c8 ignore next -- In Babel? */
case 'PropertyDefinition':
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ClassProperty':
case 'Property':
/* eslint-enable no-fallthrough */
return node.computed && hasNonEmptyResolverCall(node.key, resolverName) ||
hasNonEmptyResolverCall(node.value, resolverName);
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'ObjectMethod':
/* c8 ignore next 6 -- In Babel? */
// @ts-expect-error
return node.computed && hasNonEmptyResolverCall(node.key, resolverName) ||
// @ts-expect-error
node.arguments.some((nde) => {
return hasNonEmptyResolverCall(nde, resolverName);
});
case 'ClassExpression':
case 'ClassDeclaration':
return hasNonEmptyResolverCall(node.body, resolverName);
case 'AwaitExpression':
case 'SpreadElement':
case 'UnaryExpression':
case 'YieldExpression':
return hasNonEmptyResolverCall(node.argument, resolverName);
case 'VariableDeclaration': {
return node.declarations.some((nde) => {
return hasNonEmptyResolverCall(nde, resolverName);
});
}
case 'VariableDeclarator': {
return hasNonEmptyResolverCall(node.id, resolverName) ||
hasNonEmptyResolverCall(node.init, resolverName);
}
case 'TaggedTemplateExpression':
return hasNonEmptyResolverCall(node.quasi, resolverName);
// ?.
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'OptionalMemberExpression':
case 'MemberExpression':
return hasNonEmptyResolverCall(node.object, resolverName) ||
hasNonEmptyResolverCall(node.property, resolverName);
/* c8 ignore next 2 -- In Babel? */
// @ts-expect-error Babel?
case 'Import':
case 'ImportExpression':
return hasNonEmptyResolverCall(node.source, resolverName);
case 'ReturnStatement': {
if (node.argument === null) {
return false;
}
return hasNonEmptyResolverCall(node.argument, resolverName);
}
/*
// Shouldn't need to parse literals/literal components, etc.
case 'Identifier':
case 'TemplateElement':
case 'Super':
// Exports not relevant in this context
*/
default:
return false;
}
};
/**
* Checks if a Promise executor has no resolve value or an empty value.
* An `undefined` resolve does not count.
* @param {ESTreeOrTypeScriptNode} node
* @param {boolean} anyPromiseAsReturn
* @param {boolean} [allBranches]
* @returns {boolean}
*/
const hasValueOrExecutorHasNonEmptyResolveValue = (node, anyPromiseAsReturn, allBranches) => {
const hasReturnMethod = allBranches ?
/**
* @param {ESTreeOrTypeScriptNode} nde
* @param {PromiseFilter} promiseFilter
* @returns {boolean}
*/
(nde, promiseFilter) => {
let hasReturn;
try {
hasReturn = hasReturnValue(nde, true, promiseFilter);
} catch (error) {
// c8 ignore else
if (/** @type {Error} */ (error).message === 'Null return') {
return false;
}
/* c8 ignore next 2 */
throw error;
}
// `hasReturn` check needed since `throw` treated as valid return by
// `allBrancheshaveReturnValues`
return Boolean(hasReturn && allBrancheshaveReturnValues(nde, promiseFilter));
} :
/**
* @param {ESTreeOrTypeScriptNode} nde
* @param {PromiseFilter} promiseFilter
* @returns {boolean}
*/
(nde, promiseFilter) => {
return Boolean(hasReturnValue(nde, false, promiseFilter));
};
return hasReturnMethod(node, (prom) => {
if (anyPromiseAsReturn) {
return true;
}
if (isVoidPromise(prom)) {
return false;
}
const {
params,
body,
} =
/**
* @type {import('@typescript-eslint/types').TSESTree.FunctionExpression|
* import('@typescript-eslint/types').TSESTree.ArrowFunctionExpression}
*/ (
/** @type {import('@typescript-eslint/types').TSESTree.NewExpression} */ (
prom
).arguments[0]
) || {};
if (!params?.length) {
return false;
}
const {
name: resolverName,
} = /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
params[0]
);
return hasNonEmptyResolverCall(body, resolverName);
});
};
export {
hasReturnValue,
hasValueOrExecutorHasNonEmptyResolveValue,
};