eslint-plugin-jsdoc
Version:
JSDoc linting rules for ESLint.
1,054 lines (935 loc) • 30.3 kB
JavaScript
import iterateJsdoc, {
parseComment,
} from '../iterateJsdoc.js';
import {
getDocumentNamepathDefiningTags,
getJSDocCommentBlocks,
} from '../jsdocUtils.js';
import {
getJSDocComment,
parse as parseType,
traverse,
tryParse as tryParseType,
} from '@es-joy/jsdoccomment';
import {
parseImportsExports,
} from 'parse-imports-exports';
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', 'Intl',
];
const globalTypes = [
'globalThis', 'global', 'window', 'self',
];
const iterableIterator = [
'Iterable',
'Iterator',
'IteratorObject',
];
const typescriptGlobals = [
...iterableIterator,
// 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',
// Type-only globals of TypeScript's standard library, grouped by the
// `typescript/lib` file which first declares them
// lib.es5.d.ts
'ArrayBufferConstructor',
'ArrayBufferLike',
'ArrayBufferTypes',
'ArrayBufferView',
'ArrayConstructor',
'ArrayLike',
'BooleanConstructor',
'CallableFunction',
'ConcatArray',
'DataViewConstructor',
'DateConstructor',
'ErrorConstructor',
'EvalErrorConstructor',
'Float32ArrayConstructor',
'Float64ArrayConstructor',
'FunctionConstructor',
'IArguments',
'ImportAssertions',
'ImportAttributes',
'ImportCallOptions',
'ImportMeta',
'Int16ArrayConstructor',
'Int32ArrayConstructor',
'Int8ArrayConstructor',
'NewableFunction',
'NoInfer',
'NumberConstructor',
'ObjectConstructor',
'PromiseConstructorLike',
'PromiseLike',
'PropertyDescriptor',
'PropertyDescriptorMap',
'PropertyKey',
'RangeErrorConstructor',
'ReadonlyArray',
'ReferenceErrorConstructor',
'RegExpConstructor',
'RegExpExecArray',
'RegExpMatchArray',
'StringConstructor',
'SyntaxErrorConstructor',
'TemplateStringsArray',
'TypeErrorConstructor',
'TypedPropertyDescriptor',
'URIErrorConstructor',
'Uint16ArrayConstructor',
'Uint32ArrayConstructor',
'Uint8ArrayConstructor',
'Uint8ClampedArrayConstructor',
'WeakKey',
'WeakKeyTypes',
// lib.es2015.collection.d.ts
'MapConstructor',
'ReadonlyMap',
'ReadonlySet',
'SetConstructor',
'WeakMapConstructor',
'WeakSetConstructor',
// lib.es2015.generator.d.ts
'Generator',
'GeneratorFunction',
'GeneratorFunctionConstructor',
// lib.es2015.iterable.d.ts
'ArrayIterator',
'BuiltinIteratorReturn',
'IterableIterator',
'IteratorResult',
'IteratorReturnResult',
'IteratorYieldResult',
'MapIterator',
'PromiseConstructor',
'SetIterator',
'StringIterator',
'SymbolConstructor',
// lib.es2015.proxy.d.ts
'ProxyConstructor',
'ProxyHandler',
// lib.es2017.sharedmemory.d.ts
'SharedArrayBufferConstructor',
// lib.es2018.asyncgenerator.d.ts
'AsyncGenerator',
'AsyncGeneratorFunction',
'AsyncGeneratorFunctionConstructor',
// lib.es2018.asynciterable.d.ts
'AsyncIterable',
'AsyncIterableIterator',
'AsyncIterator',
'AsyncIteratorObject',
// lib.es2019.array.d.ts
'FlatArray',
// lib.es2020.bigint.d.ts
'BigInt64ArrayConstructor',
'BigIntConstructor',
'BigIntToLocaleStringOptions',
'BigUint64ArrayConstructor',
// lib.es2020.promise.d.ts
'PromiseFulfilledResult',
'PromiseRejectedResult',
'PromiseSettledResult',
// lib.es2020.symbol.wellknown.d.ts
'RegExpStringIterator',
// lib.es2021.promise.d.ts
'AggregateErrorConstructor',
// lib.es2021.weakref.d.ts
'FinalizationRegistryConstructor',
'WeakRefConstructor',
// lib.es2022.error.d.ts
'ErrorOptions',
// lib.es2022.regexp.d.ts
'RegExpIndicesArray',
// lib.es2024.promise.d.ts
'PromiseWithResolvers',
// lib.es2025.collection.d.ts
'ReadonlySetLike',
// lib.es2025.iterator.d.ts
'IteratorObjectConstructor',
// lib.esnext.disposable.d.ts
'AsyncDisposable',
'AsyncDisposableStackConstructor',
'Disposable',
'DisposableStackConstructor',
'SuppressedErrorConstructor',
// lib.decorators.d.ts
'ClassAccessorDecoratorContext',
'ClassAccessorDecoratorResult',
'ClassAccessorDecoratorTarget',
'ClassDecoratorContext',
'ClassFieldDecoratorContext',
'ClassGetterDecoratorContext',
'ClassMemberDecoratorContext',
'ClassMethodDecoratorContext',
'ClassSetterDecoratorContext',
'DecoratorContext',
'DecoratorMetadata',
'DecoratorMetadataObject',
// lib.decorators.legacy.d.ts
'ClassDecorator',
'MethodDecorator',
'ParameterDecorator',
'PropertyDecorator',
];
/**
* @param {string|false|undefined} [str]
* @returns {undefined|string|false}
*/
const stripPseudoTypes = (str) => {
return str && str.replace(/(?:\.|<>|\.<>|\[\])$/v, '');
};
export default iterateJsdoc(({
context,
node,
report,
settings,
sourceCode,
state,
utils,
}) => {
/** @type {string[]} */
const foundTypedefValues = [];
const {
scopeManager,
} = sourceCode;
// When is this ever `null`?
const globalScope = /** @type {import('eslint').Scope.Scope} */ (
scopeManager.globalScope
);
const
/**
* @type {{
* checkUsedTypedefs: boolean
* definedTypes: string[],
* disableReporting: boolean,
* markVariablesAsUsed: boolean,
* }}
*/ {
checkUsedTypedefs = false,
definedTypes = [],
disableReporting = false,
markVariablesAsUsed = true,
} = context.options[0] || {};
/** @type {(string|undefined)[]} */
let definedPreferredTypes = [];
const {
mode,
preferredTypes,
structuredTags,
} = 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 allComments = sourceCode.getAllComments();
const comments = getJSDocCommentBlocks(sourceCode);
const globals = allComments
.filter((comment) => {
return (/^\s*globals/v).test(comment.value);
}).flatMap((commentNode) => {
return commentNode.value.replace(/^\s*globals/v, '').trim().split(/,\s*/v);
}).concat(Object.keys(context.languageOptions.globals ?? []));
const typedefs = getDocumentNamepathDefiningTags(sourceCode);
const typedefDeclarations = typedefs
.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 {
description,
name,
type,
} = tag;
const typePart = type ? `{${type}} ` : '';
const imprt = 'import ' + (description ?
`${typePart}${name} ${description}` :
`${typePart}${name}`);
const importsExports = parseImportsExports(imprt.trim());
const types = [];
const namedImports = Object.values(importsExports.namedImports || {})[0]?.[0];
if (namedImports) {
if (namedImports.default) {
types.push(namedImports.default);
}
if (namedImports.names) {
types.push(...Object.keys(namedImports.names));
}
}
const namespaceImports = Object.values(importsExports.namespaceImports || {})[0]?.[0];
if (namespaceImports) {
if (namespaceImports.namespace) {
types.push(namespaceImports.namespace);
}
if (namespaceImports.default) {
types.push(namespaceImports.default);
}
}
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 jsdc = parseComment(commentNode, '');
return jsdc.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);
}) :
// We err on the side of being too aggressive; checking only
// present tags is not sufficient
comments.flatMap((doc) => {
return doc.tags.filter(({
tag,
}) => {
return tag === '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';
/**
* @param {import("eslint").Scope.Scope | null} scope
* @returns {Set<string>}
*/
const getValidRuntimeIdentifiers = (scope) => {
const result = new Set();
let scp = scope;
/**
* @param {import("eslint").Scope.Scope | null} sc
*/
const getChildScopes = (sc) => {
if (sc) {
// We must check child scopes because when multiple nodes find
// the same comment block, only one node is reported by our code,
// and it can be the children which are not reported.
for (const childScope of sc.childScopes) {
for (const {
name,
} of childScope.variables) {
result.add(name);
}
for (const grandChildScope of childScope.childScopes) {
getChildScopes(grandChildScope);
}
}
}
};
getChildScopes(scp);
while (scp) {
for (const {
name,
} of scp.variables) {
result.add(name);
}
scp = scp.upper;
}
return result;
};
/**
* Recursively extracts types from a namespace declaration.
* @param {string} prefix - The namespace prefix (e.g., "MyNamespace" or "Outer.Inner").
* @param {import('@typescript-eslint/types').TSESTree.TSModuleDeclaration} moduleDeclaration - The module declaration node.
* @returns {string[]} Array of fully qualified type names.
*/
const getNamespaceTypes = (prefix, moduleDeclaration) => {
/* c8 ignore next 3 -- Guard for ambient modules without body. */
if (!moduleDeclaration.body || moduleDeclaration.body.type !== 'TSModuleBlock') {
return [];
}
return moduleDeclaration.body.body.flatMap((item) => {
/** @type {import('@typescript-eslint/types').TSESTree.ProgramStatement | import('@typescript-eslint/types').TSESTree.NamedExportDeclarations | null} */
let declaration = item;
if (item.type === 'ExportNamedDeclaration' && item.declaration) {
declaration = item.declaration;
}
if (declaration.type === 'TSTypeAliasDeclaration' || declaration.type === 'ClassDeclaration') {
/* c8 ignore next 4 -- Guard for anonymous class declarations. */
if (!declaration.id) {
return [];
}
return [
`${prefix}.${declaration.id.name}`,
];
}
if (declaration.type === 'TSInterfaceDeclaration') {
return [
`${prefix}.${declaration.id.name}`,
...declaration.body.body.map((prop) => {
// Only `TSPropertySignature` and `TSMethodSignature` have 'key'.
if (prop.type !== 'TSPropertySignature' && prop.type !== 'TSMethodSignature') {
return '';
}
// Key can be computed or a literal, only handle Identifier.
if (prop.key.type !== 'Identifier') {
return '';
}
const propName = prop.key.name;
/* c8 ignore next -- `propName` is always truthy for Identifiers. */
return propName ? `${prefix}.${declaration.id.name}.${propName}` : '';
}).filter(Boolean),
];
}
// Handle nested namespaces.
if (declaration.type === 'TSModuleDeclaration') {
/* c8 ignore next -- Nested string-literal modules aren't valid TS syntax. */
const nestedName = declaration.id?.type === 'Identifier' ? declaration.id.name : '';
/* c8 ignore next 3 -- Guard. */
if (!nestedName) {
return [];
}
return [
`${prefix}.${nestedName}`,
...getNamespaceTypes(`${prefix}.${nestedName}`, declaration),
];
}
if (declaration.type === 'TSImportEqualsDeclaration') {
return [
`${prefix}.${declaration.id.name}`,
];
}
if (declaration.type === 'TSEnumDeclaration') {
return [
`${prefix}.${declaration.id.name}`,
...declaration.body.members.map((member) => {
// Only handle Identifier member names, not string literals.
if (member.id.type !== 'Identifier') {
return '';
}
return `${prefix}.${declaration.id.name}.${member.id.name}`;
}).filter(Boolean),
];
}
if (declaration.type === 'VariableDeclaration') {
return declaration.declarations.flatMap((declarator) => {
// Destructuring patterns are not handled.
if (declarator.id.type !== 'Identifier') {
return [];
}
return [
`${prefix}.${declarator.id.name}`,
];
});
}
if (declaration.type === 'FunctionDeclaration' || declaration.type === 'TSDeclareFunction') {
/* c8 ignore next 3 -- Guard for anonymous default-exported functions which can't exist in a namespace. */
if (!declaration.id) {
return [];
}
return [
`${prefix}.${declaration.id.name}`,
];
}
// Fallback for unhandled declaration types.
return [];
});
};
/**
* We treat imports differently as we can't introspect their children.
* @type {string[]}
*/
const imports = [];
/** @type {Set<string>} */
const closedTypes = new Set();
const tsModuleVariables = scopeManager.scopes.filter(({
type,
}) => {
// @ts-expect-error TS
return type === 'tsModule';
}).flatMap(({
variables,
}) => {
return variables.map(({
name,
}) => {
return name;
});
});
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;
}).flatMap(({
identifiers,
name,
}) => {
const globalItem = /** @type {import('estree').Identifier & {parent: import('@typescript-eslint/types').TSESTree.Node}} */ (
identifiers?.[0]
)?.parent;
switch (globalItem?.type) {
case 'ClassDeclaration':
closedTypes.add(name);
return [
name,
...globalItem.body.body.map((item) => {
const property = /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
/** @type {import('@typescript-eslint/types').TSESTree.PropertyDefinition} */ (
item)?.key)?.name;
/* c8 ignore next 3 -- Guard */
if (!property) {
return '';
}
return `${name}.${property}`;
}).filter(Boolean),
];
case 'ImportDefaultSpecifier':
case 'ImportNamespaceSpecifier':
case 'ImportSpecifier':
imports.push(name);
break;
case 'TSInterfaceDeclaration':
return [
name,
...globalItem.body.body.map((item) => {
const property = /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
/** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */ (
item)?.key)?.name;
/* c8 ignore next 3 -- Guard */
if (!property) {
return '';
}
return `${name}.${property}`;
}).filter(Boolean),
];
case 'TSModuleDeclaration':
closedTypes.add(name);
return [
name,
...getNamespaceTypes(name, globalItem),
];
case 'VariableDeclarator':
if (/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
/** @type {import('@typescript-eslint/types').TSESTree.CallExpression} */ (
globalItem?.init
)?.callee)?.name === 'require'
) {
imports.push(/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
globalItem.id
).name);
break;
}
// Module scope names are also defined
return [
name,
];
}
return [
name,
];
/* c8 ignore next */
}) : [],
)
.concat(extraTypes)
.concat(typedefDeclarations)
.concat(importTags)
.concat(definedTypes)
.concat(tsModuleVariables)
.concat(/** @type {string[]} */ (definedPreferredTypes))
.concat((() => {
let nodeToCheck = node;
if (node?.type === 'ExportNamedDeclaration' || node?.type === 'ExportDefaultDeclaration') {
nodeToCheck = /** @type {import('estree').ClassDeclaration & import('eslint').Rule.NodeParentExtension} */ (
/** @type {import('estree').ExportNamedDeclaration & import('eslint').Rule.NodeParentExtension} */
(nodeToCheck)?.declaration
);
}
// Other class members are not in scope, but we need them (e.g., for a
// sibling property or method referenced by `{@link}`, or a member
// referenced by `{@link}` from the class's own JSDoc block), and we
// grab them here
/** @type {import('estree').ClassBody|undefined} */
let classBody;
/** @type {string|undefined} */
let className;
if (nodeToCheck?.type === 'MethodDefinition' || nodeToCheck?.type === 'PropertyDefinition') {
classBody = /** @type {import('estree').ClassBody} */ (nodeToCheck.parent);
className = /** @type {import('estree').ClassDeclaration} */ (
nodeToCheck.parent?.parent
)?.id?.name;
} else if (nodeToCheck?.type === 'ClassDeclaration' || nodeToCheck?.type === 'ClassExpression') {
classBody = nodeToCheck.body;
className = nodeToCheck.id?.name;
}
if (classBody) {
return classBody.body.flatMap((methodOrProp) => {
if (
(methodOrProp.type === 'MethodDefinition' ||
methodOrProp.type === 'PropertyDefinition') &&
methodOrProp.key.type === 'Identifier'
) {
return [
methodOrProp.key.name,
`${className}.${methodOrProp.key.name}`,
];
}
/* c8 ignore next 2 -- Not yet built */
return '';
}).filter(Boolean);
}
return [];
})())
.concat((() => {
// Detect static property assignments like `MyClass.Prop = ...`
const programBody = /** @type {import('@typescript-eslint/types').TSESTree.Program} */ (
sourceCode.ast
).body;
return programBody.flatMap((statement) => {
if (
statement.type === 'ExpressionStatement' &&
statement.expression.type === 'AssignmentExpression' &&
statement.expression.left.type === 'MemberExpression' &&
statement.expression.left.object.type === 'Identifier' &&
statement.expression.left.property.type === 'Identifier' &&
closedTypes.has(statement.expression.left.object.name)
) {
return [
`${statement.expression.left.object.name}.${statement.expression.left.property.name}`,
];
}
return [];
});
})())
.concat(...getValidRuntimeIdentifiers(node && (
(sourceCode.getScope &&
/* c8 ignore next 3 */
sourceCode.getScope(node)) ||
// @ts-expect-error ESLint 8
context.getScope()
)))
.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 definedNamesAndNamepaths = new Set(utils.filterTags(({
tag,
}) => {
return utils.isNameOrNamepathDefiningTag(tag);
}).map(({
name,
}) => {
return name;
}));
const tagsWithTypes = /** @type {TypeAndTagInfo[]} */ ([
...typeTags,
...namepathReferencingTags,
...namepathOrUrlReferencingTags,
// Remove types which failed to parse
].filter(Boolean));
for (const {
parsedType,
tag,
} of tagsWithTypes) {
// eslint-disable-next-line complexity -- Refactor
traverse(parsedType, (nde, parentNode) => {
/**
* @type {import('jsdoc-type-pratt-parser').NameResult & {
* _parent?: import('jsdoc-type-pratt-parser').NonRootResult
* }}
*/
// eslint-disable-next-line canonical/id-match -- Avoid clashes
(nde)._parent = parentNode;
const {
type,
value,
} = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde);
let val = value;
/** @type {import('jsdoc-type-pratt-parser').NonRootResult|undefined} */
let currNode = nde;
do {
currNode =
/**
* @type {import('jsdoc-type-pratt-parser').NameResult & {
* _parent?: import('jsdoc-type-pratt-parser').NonRootResult
* }}
*/ (currNode)._parent;
if (
// Avoid appending for imports and globals since we don't want to
// check their properties which may or may not exist
!imports.includes(val) && !globals.includes(val) &&
!importTags.includes(val) &&
!extraTypes.includes(val) &&
!typedefDeclarations.includes(val) &&
!globalTypes.includes(val) &&
currNode && 'right' in currNode &&
currNode.right?.type === 'JsdocTypeProperty'
) {
val = val + '.' + currNode.right.value;
}
} while (currNode?.type === 'JsdocTypeNamePath');
if (type === 'JsdocTypeName') {
const structuredTypes = structuredTags[tag.tag]?.type;
const rootNamespace = val.split('.')[0];
const isNamespaceValid = (definedTypes.includes(rootNamespace) || allDefinedTypes.has(rootNamespace)) &&
!closedTypes.has(rootNamespace);
if (!allDefinedTypes.has(val) &&
!definedNamesAndNamepaths.has(val) &&
(!Array.isArray(structuredTypes) || !structuredTypes.includes(val)) && !isNamespaceValid
) {
const parent =
/**
* @type {import('jsdoc-type-pratt-parser').RootResult & {
* _parent?: import('jsdoc-type-pratt-parser').NonRootResult
* }}
*/ (nde)._parent;
if (parent?.type === 'JsdocTypeTypeParameter') {
return;
}
if (parent?.type === 'JsdocTypeFunction' &&
/** @type {import('jsdoc-type-pratt-parser').FunctionResult} */
(parent)?.typeParameters?.some((typeParam) => {
return value === typeParam.name.value;
})
) {
return;
}
if (parent?.type === 'JsdocTypeInfer' && value === parent.element.value) {
allDefinedTypes.add(value);
return;
}
if (!disableReporting) {
report(`The type '${val}' is undefined.`, null, tag);
}
} else if (markVariablesAsUsed && !extraTypes.includes(val)) {
if (sourceCode.markVariableAsUsed) {
sourceCode.markVariableAsUsed(val);
/* c8 ignore next 4 */
} else {
// @ts-expect-error ESLint 8
context.markVariableAsUsed(val);
}
}
if (checkUsedTypedefs && typedefDeclarations.includes(val)) {
foundTypedefValues.push(val);
}
}
});
}
state.foundTypedefValues = foundTypedefValues;
}, {
// We use this method rather than checking at end of handler above because
// in that case, it is invoked too many times and would thus report errors
// too many times.
exit ({
context,
state,
utils,
}) {
const {
checkUsedTypedefs = false,
} = context.options[0] || {};
if (!checkUsedTypedefs) {
return;
}
const allComments = context.sourceCode.getAllComments();
const comments = allComments
.filter((comment) => {
return (/^\*(?!\*)/v).test(comment.value);
})
.map((commentNode) => {
return {
doc: parseComment(commentNode, ''),
loc: commentNode.loc,
};
});
const typedefs = comments
.flatMap(({
doc,
loc,
}) => {
const tags = doc.tags.filter(({
tag,
}) => {
return utils.isNameOrNamepathDefiningTag(tag);
});
if (!tags.length) {
return [];
}
return {
loc,
tags,
};
});
for (const typedef of typedefs) {
if (
!state.foundTypedefValues.includes(typedef.tags[0].name)
) {
context.report({
loc: /** @type {import('@eslint/core').SourceLocation} */ (typedef.loc),
message: 'This typedef was not used within the file',
});
}
}
},
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Besides some expected built-in types, prohibits any types not specified as globals or within `@typedef`.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-undefined-types.md#repos-sticky-header',
},
schema: [
{
additionalProperties: false,
properties: {
checkUsedTypedefs: {
description: 'Whether to check typedefs for use within the file',
type: 'boolean',
},
definedTypes: {
description: `This array can be populated to indicate other types which
are automatically considered as defined (in addition to globals, etc.).
Defaults to an empty array.`,
items: {
type: 'string',
},
type: 'array',
},
disableReporting: {
description: `Whether to disable reporting of errors. Defaults to
\`false\`. This may be set to \`true\` in order to take advantage of only
marking defined variables as used or checking used typedefs.`,
type: 'boolean',
},
markVariablesAsUsed: {
description: `Whether to mark variables as used for the purposes
of the \`no-unused-vars\` rule when they are not found to be undefined.
Defaults to \`true\`. May be set to \`false\` to enforce a practice of not
importing types unless used in code.`,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});