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