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

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',
},
});