Skip to content

Commit

Permalink
refactor: Improve typescript types and strictness (#367)
Browse files Browse the repository at this point in the history
  • Loading branch information
scagood authored Oct 30, 2024
1 parent 06d60ae commit 18cdd53
Show file tree
Hide file tree
Showing 73 changed files with 664 additions and 384 deletions.
7 changes: 5 additions & 2 deletions lib/configs/_commons.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use strict"

module.exports.commonRules = /** @type {const} */ ({
/**
* @type {import('eslint').Linter.RulesRecord}
*/
module.exports.commonRules = {
"n/no-deprecated-api": "error",
"n/no-extraneous-import": "error",
"n/no-extraneous-require": "error",
Expand All @@ -16,4 +19,4 @@ module.exports.commonRules = /** @type {const} */ ({
"n/no-unsupported-features/node-builtins": "error",
"n/process-exit-as-throw": "error",
"n/hashbang": "error",
})
}
5 changes: 1 addition & 4 deletions lib/eslint-utils.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
declare module "eslint-plugin-es-x" {
// @ts-ignore
export const rules: NonNullable<import('eslint').ESLint.Plugin["rules"]>;
}

declare module "@eslint-community/eslint-utils" {
// @ts-ignore
import * as estree from 'estree';
// @ts-ignore
import * as eslint from 'eslint';

type Node = estree.Node | estree.Expression;
Expand Down Expand Up @@ -39,7 +36,7 @@ declare module "@eslint-community/eslint-utils" {
[READ]?: Info;
[CALL]?: Info;
[CONSTRUCT]?: Info;
[key: string]: TraceMap<Info>;
[key: string]: TraceMap<Info> | undefined;
}
type RichNode = eslint.Rule.Node | Node;
type Reference<Info extends unknown> = {
Expand Down
68 changes: 34 additions & 34 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,15 @@ const esmConfig = require("./configs/recommended-module")
const cjsConfig = require("./configs/recommended-script")
const recommendedConfig = require("./configs/recommended")

/**
* @typedef {{
'recommended-module': import('eslint').ESLint.ConfigData;
'recommended-script': import('eslint').ESLint.ConfigData;
'recommended': import('eslint').ESLint.ConfigData;
'flat/recommended-module': import('eslint').Linter.FlatConfig;
'flat/recommended-script': import('eslint').Linter.FlatConfig;
'flat/recommended': import('eslint').Linter.FlatConfig;
'flat/mixed-esm-and-cjs': import('eslint').Linter.FlatConfig[];
}} Configs
*/
/** @import { ESLint, Linter } from 'eslint' */

/** @type {import('eslint').ESLint.Plugin & { configs: Configs }} */
const plugin = {
/** @type {ESLint.Plugin} */
const base = {
meta: {
name: pkg.name,
version: pkg.version,
},
rules: /** @type {Record<string, import('eslint').Rule.RuleModule>} */ ({
rules: {
"callback-return": require("./rules/callback-return"),
"exports-style": require("./rules/exports-style"),
"file-extension-in-import": require("./rules/file-extension-in-import"),
Expand Down Expand Up @@ -66,28 +56,38 @@ const plugin = {
// Deprecated rules.
"no-hide-core-modules": require("./rules/no-hide-core-modules"),
shebang: require("./rules/shebang"),
}),
configs: {
"recommended-module": { plugins: ["n"], ...esmConfig.eslintrc },
"recommended-script": { plugins: ["n"], ...cjsConfig.eslintrc },
recommended: { plugins: ["n"], ...recommendedConfig.eslintrc },
"flat/recommended-module": { ...esmConfig.flat },
"flat/recommended-script": { ...cjsConfig.flat },
"flat/recommended": { ...recommendedConfig.flat },
"flat/mixed-esm-and-cjs": [
{ files: ["**/*.js"], ...recommendedConfig.flat },
{ files: ["**/*.mjs"], ...esmConfig.flat },
{ files: ["**/*.cjs"], ...cjsConfig.flat },
],
},
}
/**
* @typedef {{
* 'recommended-module': ESLint.ConfigData;
* 'recommended-script': ESLint.ConfigData;
* 'recommended': ESLint.ConfigData;
* 'flat/recommended-module': Linter.Config;
* 'flat/recommended-script': Linter.Config;
* 'flat/recommended': Linter.Config;
* 'flat/mixed-esm-and-cjs': Linter.Config[];
* }} Configs
*/

plugin.configs["flat/recommended-module"].plugins = { n: plugin }
plugin.configs["flat/recommended-script"].plugins = { n: plugin }
plugin.configs["flat/recommended"].plugins = { n: plugin }

for (const config of plugin.configs["flat/mixed-esm-and-cjs"]) {
config.plugins = { n: plugin }
/** @type {Configs} */
const configs = {
"recommended-module": { plugins: ["n"], ...esmConfig.eslintrc },
"recommended-script": { plugins: ["n"], ...cjsConfig.eslintrc },
recommended: { plugins: ["n"], ...recommendedConfig.eslintrc },
"flat/recommended-module": { plugins: { n: base }, ...esmConfig.flat },
"flat/recommended-script": { plugins: { n: base }, ...cjsConfig.flat },
"flat/recommended": { plugins: { n: base }, ...recommendedConfig.flat },
"flat/mixed-esm-and-cjs": [
{ files: ["**/*.js"], plugins: { n: base }, ...recommendedConfig.flat },
{ files: ["**/*.mjs"], plugins: { n: base }, ...esmConfig.flat },
{ files: ["**/*.cjs"], plugins: { n: base }, ...cjsConfig.flat },
],
}

module.exports = plugin
/** @type {ESLint.Plugin & { configs: Configs }} */
module.exports = {
meta: base.meta,
rules: base.rules,
configs: configs,
}
12 changes: 4 additions & 8 deletions lib/rules/callback-return.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ module.exports = {
/**
* Determines whether or not the callback is part of a callback expression.
* @param {import('eslint').Rule.Node} node The callback node
* @param {import('estree').Statement} parentNode The expression node
* @param {import('estree').Statement} [parentNode] The expression node
* @returns {boolean} Whether or not this is part of a callback expression
*/
function isCallbackExpression(node, parentNode) {
Expand Down Expand Up @@ -136,8 +136,7 @@ module.exports = {
// block statements are part of functions and most if statements
if (closestBlock?.type === "BlockStatement") {
// find the last item in the block
const lastItem =
closestBlock.body[closestBlock.body.length - 1]
const lastItem = closestBlock.body.at(-1)

// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
Expand All @@ -154,13 +153,10 @@ module.exports = {
}

// ending a block with a return is also ok
if (lastItem.type === "ReturnStatement") {
if (lastItem?.type === "ReturnStatement") {
// but only if the callback is immediately before
if (
isCallbackExpression(
node,
closestBlock.body[closestBlock.body.length - 2]
)
isCallbackExpression(node, closestBlock.body.at(-2))
) {
return
}
Expand Down
91 changes: 49 additions & 42 deletions lib/rules/exports-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
*/
"use strict"

/**
* @typedef {import('estree').Node & { parent?: Node }} Node
*/
const { hasParentNode } = require("../util/has-parent-node.js")

/*istanbul ignore next */
/**
* This function is copied from https://github.com/eslint/eslint/blob/2355f8d0de1d6732605420d15ddd4f1eee3c37b6/lib/ast-utils.js#L648-L684
*
* @param {Node} node - The node to get.
* @param {import('estree').Node} node - The node to get.
* @returns {string | null | undefined} The property name if static. Otherwise, null.
* @private
*/
Expand All @@ -39,17 +37,12 @@ function getStaticPropertyName(node) {

case "TemplateLiteral":
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
return prop.quasis[0].value.cooked
return prop.quasis[0]?.value.cooked
}
break

case "Identifier":
if (
!(
/** @type {import('estree').MemberExpression} */ (node)
.computed
)
) {
if (node.type === "MemberExpression" && node.computed === false) {
return prop.name
}
break
Expand All @@ -63,11 +56,12 @@ function getStaticPropertyName(node) {
/**
* Checks whether the given node is assignee or not.
*
* @param {Node} node - The node to check.
* @param {import('estree').Node} node - The node to check.
* @returns {boolean} `true` if the node is assignee.
*/
function isAssignee(node) {
return (
hasParentNode(node) &&
node.parent?.type === "AssignmentExpression" &&
node.parent.left === node
)
Expand All @@ -79,15 +73,16 @@ 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 {Node} leafNode - The node to get.
* @returns {Node|null} The top assignment expression node, or null.
* @param {import('estree').Node} leafNode - The node to get.
* @returns {import('estree').Node | null} The top assignment expression node, or null.
*/
function getTopAssignment(leafNode) {
let node = leafNode

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

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

Expand All @@ -109,35 +104,41 @@ function getTopAssignment(leafNode) {
/**
* Gets top assignment nodes of the given node list.
*
* @param {Node[]} nodes - The node list to get.
* @returns {Node[]} Gotten top assignment nodes.
* @param {import('estree').Node[]} nodes - The node list to get.
* @returns {import('estree').Node[]} Gotten top assignment nodes.
*/
function createAssignmentList(nodes) {
return /** @type {Node[]} */ (nodes.map(getTopAssignment).filter(Boolean))
return nodes.map(getTopAssignment).filter(input => input != null)
}

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

/** @type {import('estree').Node[]} */
const nodes = []

for (const reference of variable.references) {
if (hasParentNode(reference.identifier) === false) {
continue
}
const node = reference.identifier.parent
if (
node.type === "MemberExpression" &&
getStaticPropertyName(node) === "exports"
) {
nodes.push(node)
}
}
return nodes
}

/**
Expand All @@ -156,7 +157,7 @@ function getExportsNodes(scope) {
}

/**
* @param {Node} property
* @param {import('estree').Node} property
* @param {import('eslint').SourceCode} sourceCode
* @returns {string | null}
*/
Expand Down Expand Up @@ -210,31 +211,36 @@ function getReplacementForProperty(property, sourceCode) {

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

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

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

Expand Down
Loading

0 comments on commit 18cdd53

Please sign in to comment.