Skip to content

Commit

Permalink
feat: Types for lib/rules
Browse files Browse the repository at this point in the history
  • Loading branch information
scagood committed Mar 25, 2024
1 parent b6157bf commit faa3ed8
Show file tree
Hide file tree
Showing 23 changed files with 328 additions and 208 deletions.
4 changes: 4 additions & 0 deletions lib/eslint-utils.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
declare module "eslint-plugin-es-x" {
export const rules: NonNullable<import('eslint').ESLint.Plugin["rules"]>;
}

declare module "@eslint-community/eslint-utils" {
import * as estree from 'estree';
import * as eslint from 'eslint';
Expand Down
122 changes: 80 additions & 42 deletions lib/rules/exports-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,52 @@
*/
"use strict"

/**
* @typedef {import('estree').Node & { parent?: Node }} Node
*/

/*istanbul ignore next */
/**
* This function is copied from https://github.com/eslint/eslint/blob/2355f8d0de1d6732605420d15ddd4f1eee3c37b6/lib/ast-utils.js#L648-L684
*
* @param {import('eslint').Rule.Node} node - The node to get.
* @param {Node} node - The node to get.
* @returns {string | null | undefined} The property name if static. Otherwise, null.
* @private
*/
function getStaticPropertyName(node) {
/** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */
let prop = null

switch (node?.type) {
case "Property":
case "MethodDefinition":
prop = /** @type {import('estree').Property} */ (node).key
prop = node.key
break

case "MemberExpression":
prop = /** @type {import('estree').MemberExpression} */ (node)
.property
prop = node.property
break

// no default
}

switch (prop?.type) {
case "Literal":
return String(/** @type {import('estree').Literal} */ (prop).value)
return String(prop.value)

case "TemplateLiteral":
if (
/** @type {import('estree').TemplateLiteral} */ (prop)
.expressions.length === 0 &&
/** @type {import('estree').TemplateLiteral} */ (prop).quasis
.length === 1
) {
return /** @type {import('estree').TemplateLiteral} */ (prop)
.quasis[0].value.cooked
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
return prop.quasis[0].value.cooked
}
break

case "Identifier":
if (!node.computed) {
if (
!(
/** @type {import('estree').MemberExpression} */ (node)
.computed
)
) {
return prop.name
}
break
Expand All @@ -60,12 +63,13 @@ function getStaticPropertyName(node) {
/**
* Checks whether the given node is assignee or not.
*
* @param {import('eslint').Rule.Node} node - The node to check.
* @param {Node} node - The node to check.
* @returns {boolean} `true` if the node is assignee.
*/
function isAssignee(node) {
return (
node.parent.type === "AssignmentExpression" && node.parent.left === node
node.parent?.type === "AssignmentExpression" &&
node.parent.left === node
)
}

Expand All @@ -75,15 +79,15 @@ function isAssignee(node) {
* This is used to distinguish 2 assignees belong to the same assignment.
* If the node is not an assignee, this returns null.
*
* @param {import('eslint').Rule.Node} leafNode - The node to get.
* @returns {import('eslint').Rule.Node|null} The top assignment expression node, or null.
* @param {Node} leafNode - The node to get.
* @returns {Node|null} The top assignment expression node, or null.
*/
function getTopAssignment(leafNode) {
let node = leafNode

// Skip MemberExpressions.
while (
node.parent.type === "MemberExpression" &&
node.parent?.type === "MemberExpression" &&
node.parent.object === node
) {
node = node.parent
Expand All @@ -95,7 +99,7 @@ function getTopAssignment(leafNode) {
}

// Find the top.
while (node.parent.type === "AssignmentExpression") {
while (node.parent?.type === "AssignmentExpression") {
node = node.parent
}

Expand All @@ -105,31 +109,33 @@ function getTopAssignment(leafNode) {
/**
* Gets top assignment nodes of the given node list.
*
* @param {import('eslint').Rule.Node[]} nodes - The node list to get.
* @returns {import('eslint').Rule.Node[]} Gotten top assignment nodes.
* @param {Node[]} nodes - The node list to get.
* @returns {Node[]} Gotten top assignment nodes.
*/
function createAssignmentList(nodes) {
return /** @type {import('eslint').Rule.Node[]} */ (
nodes.map(getTopAssignment).filter(Boolean)
)
return /** @type {Node[]} */ (nodes.map(getTopAssignment).filter(Boolean))
}

/**
* Gets the reference of `module.exports` from the given scope.
*
* @param {import('eslint').Scope.Scope} scope - The scope to get.
* @returns {import('eslint').Rule.Node[]} Gotten MemberExpression node list.
* @returns {Node[]} Gotten MemberExpression node list.
*/
function getModuleExportsNodes(scope) {
const variable = scope.set.get("module")
if (variable == null) {
return []
}
return variable.references
.map(reference => reference.identifier.parent)
.map(
reference =>
/** @type {Node & { parent: Node }} */ (reference.identifier)
.parent
)
.filter(
node =>
node.type === "MemberExpression" &&
node?.type === "MemberExpression" &&
getStaticPropertyName(node) === "exports"
)
}
Expand All @@ -149,6 +155,11 @@ function getExportsNodes(scope) {
return variable.references.map(reference => reference.identifier)
}

/**
* @param {Node} property
* @param {import('eslint').SourceCode} sourceCode
* @returns {string | null}
*/
function getReplacementForProperty(property, sourceCode) {
if (property.type !== "Property" || property.kind !== "init") {
// We don't have a nice syntax for adding these directly on the exports object. Give up on fixing the whole thing:
Expand All @@ -162,7 +173,7 @@ function getReplacementForProperty(property, sourceCode) {
}

let fixedValue = sourceCode.getText(property.value)
if (property.method) {
if (property.value.type === "FunctionExpression" && property.method) {
fixedValue = `function${
property.value.generator ? "*" : ""
} ${fixedValue}`
Expand All @@ -172,6 +183,7 @@ function getReplacementForProperty(property, sourceCode) {
}
const lines = sourceCode
.getCommentsBefore(property)
// @ts-expect-error getText supports both BaseNode and BaseNodeWithoutComments
.map(comment => sourceCode.getText(comment))
if (property.key.type === "Literal" || property.computed) {
// String or dynamic key:
Expand All @@ -190,28 +202,43 @@ function getReplacementForProperty(property, sourceCode) {
lines.push(
...sourceCode
.getCommentsAfter(property)
// @ts-expect-error getText supports both BaseNode and BaseNodeWithoutComments
.map(comment => sourceCode.getText(comment))
)
return lines.join("\n")
}

// Check for a top level module.exports = { ... }
/**
* Check for a top level module.exports = { ... }
* @param {Node} node
* @returns {node is {parent: import('estree').AssignmentExpression & {parent: import('estree').ExpressionStatement, right: import('estree').ObjectExpression}}}
*/
function isModuleExportsObjectAssignment(node) {
return (
node.parent.type === "AssignmentExpression" &&
node.parent.parent.type === "ExpressionStatement" &&
node.parent.parent.parent.type === "Program" &&
node.parent?.type === "AssignmentExpression" &&
node.parent?.parent?.type === "ExpressionStatement" &&
node.parent.parent.parent?.type === "Program" &&
node.parent.right.type === "ObjectExpression"
)
}

// Check for module.exports.foo or module.exports.bar reference or assignment
/**
* Check for module.exports.foo or module.exports.bar reference or assignment
* @param {Node} node
* @returns {node is import('estree').MemberExpression}
*/
function isModuleExportsReference(node) {
return (
node.parent.type === "MemberExpression" && node.parent.object === node
node.parent?.type === "MemberExpression" && node.parent.object === node
)
}

/**
* @param {Node} node
* @param {import('eslint').SourceCode} sourceCode
* @param {import('eslint').Rule.RuleFixer} fixer
* @returns {import('eslint').Rule.Fix | null}
*/
function fixModuleExports(node, sourceCode, fixer) {
if (isModuleExportsReference(node)) {
return fixer.replaceText(node, "exports")
Expand Down Expand Up @@ -280,14 +307,16 @@ module.exports = {
* module.exports = foo
* ^^^^^^^^^^^^^^^^
*
* @param {import('eslint').Rule.Node} node - The node of `exports`/`module.exports`.
* @returns {Location} The location info of reports.
* @param {Node} node - The node of `exports`/`module.exports`.
* @returns {import('estree').SourceLocation} The location info of reports.
*/
function getLocation(node) {
const token = sourceCode.getTokenAfter(node)
return {
start: node.loc.start,
end: token.loc.end,
start: /** @type {import('estree').SourceLocation} */ (node.loc)
.start,
end: /** @type {import('estree').SourceLocation} */ (token?.loc)
?.end,
}
}

Expand All @@ -306,9 +335,11 @@ module.exports = {

for (const node of exportsNodes) {
// Skip if it's a batch assignment.
const topAssignment = getTopAssignment(node)
if (
topAssignment &&
assignList.length > 0 &&
assignList.indexOf(getTopAssignment(node)) !== -1
assignList.indexOf(topAssignment) !== -1
) {
continue
}
Expand Down Expand Up @@ -340,7 +371,10 @@ module.exports = {
for (const node of moduleExportsNodes) {
// Skip if it's a batch assignment.
if (assignList.length > 0) {
const found = assignList.indexOf(getTopAssignment(node))
const topAssignment = getTopAssignment(node)
const found = topAssignment
? assignList.indexOf(topAssignment)
: -1
if (found !== -1) {
batchAssignList.push(assignList[found])
assignList.splice(found, 1)
Expand All @@ -366,8 +400,12 @@ module.exports = {
continue
}

const topAssignment = getTopAssignment(node)
// Check if it's a batch assignment.
if (batchAssignList.indexOf(getTopAssignment(node)) !== -1) {
if (
topAssignment &&
batchAssignList.indexOf(topAssignment) !== -1
) {
continue
}

Expand Down
5 changes: 4 additions & 1 deletion lib/rules/file-extension-in-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ module.exports = {
*/
function verify({ filePath, name, node, moduleType }) {
// Ignore if it's not resolved to a file or it's a bare module.
if (moduleType !== "relative" && moduleType !== "absolute") {
if (
(moduleType !== "relative" && moduleType !== "absolute") ||
filePath == null
) {
return
}

Expand Down
27 changes: 11 additions & 16 deletions lib/rules/global-require.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,28 @@ const ACCEPTABLE_PARENTS = [

/**
* Finds the eslint-scope reference in the given scope.
* @param {Object} scope The scope to search.
* @param {ASTNode} node The identifier node.
* @returns {Reference|null} Returns the found reference or null if none were found.
* @param {import('eslint').Scope.Scope} scope The scope to search.
* @param {import('estree').Node} node The identifier node.
* @returns {import('eslint').Scope.Reference|undefined} Returns the found reference or null if none were found.
*/
function findReference(scope, node) {
const references = scope.references.filter(
return scope.references.find(
reference =>
reference.identifier.range[0] === node.range[0] &&
reference.identifier.range[1] === node.range[1]
reference.identifier.range?.[0] === node.range?.[0] &&
reference.identifier.range?.[1] === node.range?.[1]
)

/* istanbul ignore else: correctly returns null */
if (references.length === 1) {
return references[0]
}
return null
}

/**
* Checks if the given identifier node is shadowed in the given scope.
* @param {Object} scope The current scope.
* @param {ASTNode} node The identifier node to check.
* @param {import('eslint').Scope.Scope} scope The current scope.
* @param {import('estree').Node} node The identifier node to check.
* @returns {boolean} Whether or not the name is shadowed.
*/
function isShadowed(scope, node) {
const reference = findReference(scope, node)

return reference && reference.resolved && reference.resolved.defs.length > 0
return Boolean(reference?.resolved?.defs?.length)
}

/** @type {import('eslint').Rule.RuleModule} */
Expand Down Expand Up @@ -73,7 +67,8 @@ module.exports = {
sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9

if (
node.callee.name === "require" &&
/** @type {import('estree').Identifier} */ (node.callee)
.name === "require" &&
!isShadowed(currentScope, node.callee)
) {
const isGoodRequire = (
Expand Down
6 changes: 3 additions & 3 deletions lib/rules/handle-callback-err.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ module.exports = {

/**
* Get the parameters of a given function scope.
* @param {Object} scope The function scope.
* @returns {Array} All parameters of the given scope.
* @param {import('eslint').Scope.Scope} scope The function scope.
* @returns {import('eslint').Scope.Variable[]} All parameters of the given scope.
*/
function getParameters(scope) {
return scope.variables.filter(
Expand All @@ -67,7 +67,7 @@ module.exports = {

/**
* Check to see if we're handling the error object properly.
* @param {ASTNode} node The AST node to check.
* @param {import('estree').Node} node The AST node to check.
* @returns {void}
*/
function checkForError(node) {
Expand Down

0 comments on commit faa3ed8

Please sign in to comment.