eslint-plugin-jsdoc
Version:
JSDoc linting rules for ESLint.
233 lines (201 loc) • 6.71 kB
JavaScript
/* eslint-disable jsdoc/reject-any-type -- TypeScript types need not be installed */
import iterateJsdoc from '../iterateJsdoc.js';
import {
createRequire,
} from 'module';
let warned = false;
/** @type {any} */
let ts;
// 1. Create a require function bound to the current file's URL
const require = createRequire(import.meta.url);
try {
// 2. Attempt to import the package synchronously
ts = require('typescript');
/* c8 ignore next 10 -- Guard */
} catch (error) {
// 3. Fall back gracefully if it is not installed
if (/** @type {{code?: string}} */ (error).code !== 'MODULE_NOT_FOUND') {
// Re-throw if it's a different error (e.g., syntax error inside the package)
throw error;
}
// eslint-disable-next-line no-console -- Warning user
console.warn('⚠️ typescript is not installed. `jsdoc/no-unnecessary-type-assertion` will not work. To disable this warning, you must disable the rule.');
}
// Helper to check for standard literals and boolean/enum/template literals
/**
* @param {any} type The type is `ts.Type`
* @returns {boolean}
*/
const isLiteralType = (type) => {
// Standard primitive literals (string, number, bigint, boolean literal flags)
if (type.isLiteral()) {
return true;
}
// Check using TypeFlags for specific edge cases like boolean or template literals
// eslint-disable-next-line no-bitwise -- Convenient
if (type.flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.TemplateLiteral)) {
return true;
}
// If dealing with a union (e.g. 'a' | 'b'), you can check if all constituents are literals
if (type.isUnion()) {
return type.types.every(
/**
* @param {any} typ
*/
(typ) => {
return isLiteralType(typ);
},
);
}
return false;
};
export default iterateJsdoc(({
context,
node: nde,
report,
utils,
}) => {
/* c8 ignore next 4 -- Guard */
// Already handled
if (!ts) {
return;
}
const types = utils.getTags('type');
// 1. Only proceed if there is a @type tag
if (!types.length) {
return;
}
const {
// https://typescript-eslint.io/rules/no-unnecessary-type-assertion/
checkLiteralConstAssertions = false,
treatAnyAsRedundant = false,
typesToIgnore = [],
} = context.options[0] ?? {};
const node =
/**
* @type {import('@typescript-eslint/utils').TSESTree.Node}
*/ (nde);
// 2. Safely get TypeScript parser services and TypeChecker
const services =
/**
* @type {import('@typescript-eslint/utils').ParserServices}
*/ (context.sourceCode.parserServices);
/* c8 ignore next 10 -- Guard */
if (!services || !services.program) {
if (!warned) {
// Cannot proceed without type-aware linting enabled
// eslint-disable-next-line no-console -- Feedback
console.warn('⚠️ You must point ESLint to the `typescript-eslint` parser using `languageOptions`. See the documentation for `jsdoc/no-unnecessary-type-assertion`.');
warned = true;
}
return;
}
// 1. Grab the parser services and the active TypeChecker
// const services = ESLintUtils.getParserServices(context);
const checker = services.program.getTypeChecker();
/**
* @param {string} typeStr
* @returns {any} `ts.Type`
*/
const getTypeFromString = (typeStr) => {
// 2. Parse the type string into a virtual TypeScript SourceFile
const virtualSourceFile = ts.createSourceFile(
'inline-type-eval.ts',
`type __TargetType = ${typeStr};`,
ts.ScriptTarget.Latest,
true,
);
// 3. Find the TypeNode in the AST (type __TargetType = <TypeNode>;)
// Type `ts.TypeAliasDeclaration`
const typeAlias = /** @type {any} */ (
virtualSourceFile.statements[0]
);
const typeNode = typeAlias.type;
// 4. Resolve the type using the TypeChecker
// Note: If the type string references external symbols or types from your
// actual codebase, the checker may require a complete program context to resolve them.
return checker.getTypeFromTypeNode(typeNode);
};
// Todo: Support more than just VariableDeclaration!
// 3. For this example, let's assume we are checking VariableDeclarators
// e.g., `/** @type {number} */ const x = 5;`
if (node?.type === 'VariableDeclaration') {
const decl = node.declarations[0];
/* c8 ignore next 4 -- How to cover? */
if (!decl.init) {
// No initializer, type is likely `any`, so @type isn't redundant
return;
}
// 4. Map the ESLint AST nodes to TypeScript AST nodes
// const tsNode = services.esTreeNodeToTSNodeMap.get(decl.id);
const tsInit = services.esTreeNodeToTSNodeMap.get(decl.init);
// 5. Get the types
// tsNode type includes the JSDoc explicit type
// tsInit type is the purely inferred type from the right-hand side
const inferredType = checker.getTypeAtLocation(tsInit);
// 6. Compare the types.
const declaredTypeStr = types[0].type;
const declaredType = getTypeFromString(declaredTypeStr);
const isRedundantType = checker.isTypeAssignableTo(
inferredType,
declaredType,
);
if (declaredTypeStr === 'const') {
if (checkLiteralConstAssertions && isLiteralType(inferredType)) {
report(
'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically for literals.',
null,
types[0],
{
type: declaredTypeStr,
},
);
}
return;
}
if (
isRedundantType && (treatAnyAsRedundant || declaredTypeStr !== 'any') &&
!typesToIgnore.includes(declaredTypeStr)
) {
report(
'The @type tag declaring "{{ type }}" is redundant as TypeScript infers it automatically.',
null,
types[0],
{
type: declaredTypeStr,
},
);
}
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports redundant @type tags that match or broaden the naturally inferred TypeScript type.',
},
schema: [
{
additionalProperties: false,
properties: {
checkLiteralConstAssertions: {
description: 'Whether to check `const` type assertions as redundant',
type: 'boolean',
},
treatAnyAsRedundant: {
description: 'Whether to treat `any` type casts as redundant',
type: 'boolean',
},
typesToIgnore: {
description: 'An array list of types to ignore',
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});