This commit is contained in:
Lilith 2024-06-13 00:09:21 +02:00
parent eddf7cecb8
commit aea798d119
Signed by: lilith
GPG key ID: 8712A0F317C37175
16631 changed files with 1480363 additions and 257 deletions

View file

@ -0,0 +1,21 @@
"use strict"
module.exports = {
commonRules: {
"n/no-deprecated-api": "error",
"n/no-extraneous-import": "error",
"n/no-extraneous-require": "error",
"n/no-exports-assign": "error",
"n/no-missing-import": "error",
"n/no-missing-require": "error",
"n/no-process-exit": "error",
"n/no-unpublished-bin": "error",
"n/no-unpublished-import": "error",
"n/no-unpublished-require": "error",
"n/no-unsupported-features/es-builtins": "error",
"n/no-unsupported-features/es-syntax": "error",
"n/no-unsupported-features/node-builtins": "error",
"n/process-exit-as-throw": "error",
"n/shebang": "error",
},
}

View file

@ -0,0 +1,43 @@
"use strict"
const globals = require("globals")
const { commonRules } = require("./_commons")
// eslintrc config: https://eslint.org/docs/latest/use/configure/configuration-files
module.exports.eslintrc = {
env: {
node: true,
},
globals: {
...globals.es2021,
__dirname: "off",
__filename: "off",
exports: "off",
module: "off",
require: "off",
},
parserOptions: {
ecmaFeatures: { globalReturn: false },
ecmaVersion: 2021,
sourceType: "module",
},
rules: {
...commonRules,
"n/no-unsupported-features/es-syntax": [
"error",
{ ignores: ["modules"] },
],
},
}
// flat config: https://eslint.org/docs/latest/use/configure/configuration-files-new
module.exports.flat = {
languageOptions: {
sourceType: "module",
globals: {
...globals.node,
...module.exports.eslintrc.globals,
},
},
rules: module.exports.eslintrc.rules,
}

View file

@ -0,0 +1,40 @@
"use strict"
const globals = require("globals")
const { commonRules } = require("./_commons")
// eslintrc config: https://eslint.org/docs/latest/use/configure/configuration-files
module.exports.eslintrc = {
env: {
node: true,
},
globals: {
...globals.es2021,
__dirname: "readonly",
__filename: "readonly",
exports: "writable",
module: "readonly",
require: "readonly",
},
parserOptions: {
ecmaFeatures: { globalReturn: true },
ecmaVersion: 2021,
sourceType: "script",
},
rules: {
...commonRules,
"n/no-unsupported-features/es-syntax": ["error", { ignores: [] }],
},
}
// https://eslint.org/docs/latest/use/configure/configuration-files-new
module.exports.flat = {
languageOptions: {
sourceType: "commonjs",
globals: {
...globals.node,
...module.exports.eslintrc.globals,
},
},
rules: module.exports.eslintrc.rules,
}

View file

@ -0,0 +1,19 @@
"use strict"
const getPackageJson = require("../util/get-package-json")
const moduleConfig = require("./recommended-module")
const scriptConfig = require("./recommended-script")
const packageJson = getPackageJson()
const isModule = (packageJson && packageJson.type) === "module"
const recommendedConfig = isModule ? moduleConfig : scriptConfig
module.exports.eslintrc = {
...recommendedConfig.eslintrc,
overrides: [
{ files: ["*.cjs", ".*.cjs"], ...scriptConfig.eslintrc },
{ files: ["*.mjs", ".*.mjs"], ...moduleConfig.eslintrc },
],
}
module.exports.flat = recommendedConfig.flat

File diff suppressed because it is too large Load diff

76
home/ags/node_modules/eslint-plugin-n/lib/index.js generated vendored Normal file
View file

@ -0,0 +1,76 @@
"use strict"
const pkg = require("../package.json")
const esmConfig = require("./configs/recommended-module")
const cjsConfig = require("./configs/recommended-script")
const recommendedConfig = require("./configs/recommended")
const rules = {
"callback-return": require("./rules/callback-return"),
"exports-style": require("./rules/exports-style"),
"file-extension-in-import": require("./rules/file-extension-in-import"),
"global-require": require("./rules/global-require"),
"handle-callback-err": require("./rules/handle-callback-err"),
"no-callback-literal": require("./rules/no-callback-literal"),
"no-deprecated-api": require("./rules/no-deprecated-api"),
"no-exports-assign": require("./rules/no-exports-assign"),
"no-extraneous-import": require("./rules/no-extraneous-import"),
"no-extraneous-require": require("./rules/no-extraneous-require"),
"no-missing-import": require("./rules/no-missing-import"),
"no-missing-require": require("./rules/no-missing-require"),
"no-mixed-requires": require("./rules/no-mixed-requires"),
"no-new-require": require("./rules/no-new-require"),
"no-path-concat": require("./rules/no-path-concat"),
"no-process-env": require("./rules/no-process-env"),
"no-process-exit": require("./rules/no-process-exit"),
"no-restricted-import": require("./rules/no-restricted-import"),
"no-restricted-require": require("./rules/no-restricted-require"),
"no-sync": require("./rules/no-sync"),
"no-unpublished-bin": require("./rules/no-unpublished-bin"),
"no-unpublished-import": require("./rules/no-unpublished-import"),
"no-unpublished-require": require("./rules/no-unpublished-require"),
"no-unsupported-features/es-builtins": require("./rules/no-unsupported-features/es-builtins"),
"no-unsupported-features/es-syntax": require("./rules/no-unsupported-features/es-syntax"),
"no-unsupported-features/node-builtins": require("./rules/no-unsupported-features/node-builtins"),
"prefer-global/buffer": require("./rules/prefer-global/buffer"),
"prefer-global/console": require("./rules/prefer-global/console"),
"prefer-global/process": require("./rules/prefer-global/process"),
"prefer-global/text-decoder": require("./rules/prefer-global/text-decoder"),
"prefer-global/text-encoder": require("./rules/prefer-global/text-encoder"),
"prefer-global/url-search-params": require("./rules/prefer-global/url-search-params"),
"prefer-global/url": require("./rules/prefer-global/url"),
"prefer-promises/dns": require("./rules/prefer-promises/dns"),
"prefer-promises/fs": require("./rules/prefer-promises/fs"),
"process-exit-as-throw": require("./rules/process-exit-as-throw"),
shebang: require("./rules/shebang"),
// Deprecated rules.
"no-hide-core-modules": require("./rules/no-hide-core-modules"),
"no-unsupported-features": require("./rules/no-unsupported-features"),
}
const mod = {
meta: {
name: pkg.name,
version: pkg.version,
},
rules,
}
// set configs, e.g. mod.configs["recommended-module"]
// do not defined in the mod obj - to avoid circular dependency
mod.configs = {
"recommended-module": { plugins: ["n"], ...esmConfig.eslintrc },
"recommended-script": { plugins: ["n"], ...cjsConfig.eslintrc },
recommended: { plugins: ["n"], ...recommendedConfig.eslintrc },
"flat/recommended-module": { plugins: { n: mod }, ...esmConfig.flat },
"flat/recommended-script": { plugins: { n: mod }, ...cjsConfig.flat },
"flat/recommended": { plugins: { n: mod }, ...recommendedConfig.flat },
"flat/mixed-esm-and-cjs": [
{ plugins: { n: mod }, files: ["**/*.js"], ...recommendedConfig.flat },
{ plugins: { n: mod }, files: ["**/*.mjs"], ...esmConfig.flat },
{ plugins: { n: mod }, files: ["**/*.cjs"], ...cjsConfig.flat },
],
}
module.exports = mod

View file

@ -0,0 +1,183 @@
/**
* @author Jamund Ferguson
* See LICENSE file in root directory for full license.
*/
"use strict"
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "require `return` statements after callbacks",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/callback-return.md",
},
schema: [
{
type: "array",
items: { type: "string" },
},
],
fixable: null,
messages: {
missingReturn: "Expected return with your callback function.",
},
},
create(context) {
const callbacks = context.options[0] || ["callback", "cb", "next"]
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
/**
* Find the closest parent matching a list of types.
* @param {ASTNode} node The node whose parents we are searching
* @param {Array} types The node types to match
* @returns {ASTNode} The matched node or undefined.
*/
function findClosestParentOfType(node, types) {
if (!node.parent) {
return null
}
if (types.indexOf(node.parent.type) === -1) {
return findClosestParentOfType(node.parent, types)
}
return node.parent
}
/**
* Check to see if a node contains only identifers
* @param {ASTNode} node The node to check
* @returns {boolean} Whether or not the node contains only identifers
*/
function containsOnlyIdentifiers(node) {
if (node.type === "Identifier") {
return true
}
if (node.type === "MemberExpression") {
if (node.object.type === "Identifier") {
return true
}
if (node.object.type === "MemberExpression") {
return containsOnlyIdentifiers(node.object)
}
}
return false
}
/**
* Check to see if a CallExpression is in our callback list.
* @param {ASTNode} node The node to check against our callback names list.
* @returns {boolean} Whether or not this function matches our callback name.
*/
function isCallback(node) {
return (
containsOnlyIdentifiers(node.callee) &&
callbacks.indexOf(sourceCode.getText(node.callee)) > -1
)
}
/**
* Determines whether or not the callback is part of a callback expression.
* @param {ASTNode} node The callback node
* @param {ASTNode} parentNode The expression node
* @returns {boolean} Whether or not this is part of a callback expression
*/
function isCallbackExpression(node, parentNode) {
// ensure the parent node exists and is an expression
if (!parentNode || parentNode.type !== "ExpressionStatement") {
return false
}
// cb()
if (parentNode.expression === node) {
return true
}
// special case for cb && cb() and similar
if (
parentNode.expression.type === "BinaryExpression" ||
parentNode.expression.type === "LogicalExpression"
) {
if (parentNode.expression.right === node) {
return true
}
}
return false
}
return {
CallExpression(node) {
// if we're not a callback we can return
if (!isCallback(node)) {
return
}
// find the closest block, return or loop
const closestBlock =
findClosestParentOfType(node, [
"BlockStatement",
"ReturnStatement",
"ArrowFunctionExpression",
]) || {}
// if our parent is a return we know we're ok
if (closestBlock.type === "ReturnStatement") {
return
}
// arrow functions don't always have blocks and implicitly return
if (closestBlock.type === "ArrowFunctionExpression") {
return
}
// 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]
// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
const parentType = closestBlock.parent.type
// but only if the block is part of a function
if (
parentType === "FunctionExpression" ||
parentType === "FunctionDeclaration" ||
parentType === "ArrowFunctionExpression"
) {
return
}
}
// ending a block with a return is also ok
if (lastItem.type === "ReturnStatement") {
// but only if the callback is immediately before
if (
isCallbackExpression(
node,
closestBlock.body[closestBlock.body.length - 2]
)
) {
return
}
}
}
// as long as you're the child of a function at this point you should be asked to return
if (
findClosestParentOfType(node, [
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
])
) {
context.report({ node, messageId: "missingReturn" })
}
},
}
},
}

View file

@ -0,0 +1,387 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
/*istanbul ignore next */
/**
* This function is copied from https://github.com/eslint/eslint/blob/2355f8d0de1d6732605420d15ddd4f1eee3c37b6/lib/ast-utils.js#L648-L684
*
* @param {ASTNode} node - The node to get.
* @returns {string|null} The property name if static. Otherwise, null.
* @private
*/
function getStaticPropertyName(node) {
let prop = null
switch (node && node.type) {
case "Property":
case "MethodDefinition":
prop = node.key
break
case "MemberExpression":
prop = node.property
break
// no default
}
switch (prop && prop.type) {
case "Literal":
return String(prop.value)
case "TemplateLiteral":
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
return prop.quasis[0].value.cooked
}
break
case "Identifier":
if (!node.computed) {
return prop.name
}
break
// no default
}
return null
}
/**
* Checks whether the given node is assignee or not.
*
* @param {ASTNode} 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
)
}
/**
* Gets the top assignment expression node if the given node is an assignee.
*
* This is used to distinguish 2 assignees belong to the same assignment.
* If the node is not an assignee, this returns null.
*
* @param {ASTNode} leafNode - The node to get.
* @returns {ASTNode|null} The top assignment expression node, or null.
*/
function getTopAssignment(leafNode) {
let node = leafNode
// Skip MemberExpressions.
while (
node.parent.type === "MemberExpression" &&
node.parent.object === node
) {
node = node.parent
}
// Check assignments.
if (!isAssignee(node)) {
return null
}
// Find the top.
while (node.parent.type === "AssignmentExpression") {
node = node.parent
}
return node
}
/**
* Gets top assignment nodes of the given node list.
*
* @param {ASTNode[]} nodes - The node list to get.
* @returns {ASTNode[]} Gotten top assignment nodes.
*/
function createAssignmentList(nodes) {
return nodes.map(getTopAssignment).filter(Boolean)
}
/**
* Gets the reference of `module.exports` from the given scope.
*
* @param {escope.Scope} scope - The scope to get.
* @returns {ASTNode[]} 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)
.filter(
node =>
node.type === "MemberExpression" &&
getStaticPropertyName(node) === "exports"
)
}
/**
* Gets the reference of `exports` from the given scope.
*
* @param {escope.Scope} scope - The scope to get.
* @returns {ASTNode[]} Gotten Identifier node list.
*/
function getExportsNodes(scope) {
const variable = scope.set.get("exports")
if (variable == null) {
return []
}
return variable.references.map(reference => reference.identifier)
}
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:
// property.kind === 'get':
// module.exports = { get foo() { ... } }
// property.kind === 'set':
// module.exports = { set foo() { ... } }
// property.type === 'SpreadElement':
// module.exports = { ...foo }
return null
}
let fixedValue = sourceCode.getText(property.value)
if (property.method) {
fixedValue = `function${
property.value.generator ? "*" : ""
} ${fixedValue}`
if (property.value.async) {
fixedValue = `async ${fixedValue}`
}
}
const lines = sourceCode
.getCommentsBefore(property)
.map(comment => sourceCode.getText(comment))
if (property.key.type === "Literal" || property.computed) {
// String or dynamic key:
// module.exports = { [ ... ]: ... } or { "foo": ... }
lines.push(
`exports[${sourceCode.getText(property.key)}] = ${fixedValue};`
)
} else if (property.key.type === "Identifier") {
// Regular identifier:
// module.exports = { foo: ... }
lines.push(`exports.${property.key.name} = ${fixedValue};`)
} else {
// Some other unknown property type. Conservatively give up on fixing the whole thing.
return null
}
lines.push(
...sourceCode
.getCommentsAfter(property)
.map(comment => sourceCode.getText(comment))
)
return lines.join("\n")
}
// Check for a top level module.exports = { ... }
function isModuleExportsObjectAssignment(node) {
return (
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
function isModuleExportsReference(node) {
return (
node.parent.type === "MemberExpression" && node.parent.object === node
)
}
function fixModuleExports(node, sourceCode, fixer) {
if (isModuleExportsReference(node)) {
return fixer.replaceText(node, "exports")
}
if (!isModuleExportsObjectAssignment(node)) {
return null
}
const statements = []
const properties = node.parent.right.properties
for (const property of properties) {
const statement = getReplacementForProperty(property, sourceCode)
if (statement) {
statements.push(statement)
} else {
// No replacement available, give up on the whole thing
return null
}
}
return fixer.replaceText(node.parent, statements.join("\n\n"))
}
module.exports = {
meta: {
docs: {
description: "enforce either `module.exports` or `exports`",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/exports-style.md",
},
type: "suggestion",
fixable: "code",
schema: [
{
//
enum: ["module.exports", "exports"],
},
{
type: "object",
properties: { allowBatchAssign: { type: "boolean" } },
additionalProperties: false,
},
],
messages: {
unexpectedExports:
"Unexpected access to 'exports'. Use 'module.exports' instead.",
unexpectedModuleExports:
"Unexpected access to 'module.exports'. Use 'exports' instead.",
unexpectedAssignment:
"Unexpected assignment to 'exports'. Don't modify 'exports' itself.",
},
},
create(context) {
const mode = context.options[0] || "module.exports"
const batchAssignAllowed = Boolean(
context.options[1] != null && context.options[1].allowBatchAssign
)
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
/**
* Gets the location info of reports.
*
* exports = foo
* ^^^^^^^^^
*
* module.exports = foo
* ^^^^^^^^^^^^^^^^
*
* @param {ASTNode} node - The node of `exports`/`module.exports`.
* @returns {Location} The location info of reports.
*/
function getLocation(node) {
const token = sourceCode.getTokenAfter(node)
return {
start: node.loc.start,
end: token.loc.end,
}
}
/**
* Enforces `module.exports`.
* This warns references of `exports`.
*
* @returns {void}
*/
function enforceModuleExports(globalScope) {
const exportsNodes = getExportsNodes(globalScope)
const assignList = batchAssignAllowed
? createAssignmentList(getModuleExportsNodes(globalScope))
: []
for (const node of exportsNodes) {
// Skip if it's a batch assignment.
if (
assignList.length > 0 &&
assignList.indexOf(getTopAssignment(node)) !== -1
) {
continue
}
// Report.
context.report({
node,
loc: getLocation(node),
messageId: "unexpectedExports",
})
}
}
/**
* Enforces `exports`.
* This warns references of `module.exports`.
*
* @returns {void}
*/
function enforceExports(globalScope) {
const exportsNodes = getExportsNodes(globalScope)
const moduleExportsNodes = getModuleExportsNodes(globalScope)
const assignList = batchAssignAllowed
? createAssignmentList(exportsNodes)
: []
const batchAssignList = []
for (const node of moduleExportsNodes) {
// Skip if it's a batch assignment.
if (assignList.length > 0) {
const found = assignList.indexOf(getTopAssignment(node))
if (found !== -1) {
batchAssignList.push(assignList[found])
assignList.splice(found, 1)
continue
}
}
// Report.
context.report({
node,
loc: getLocation(node),
messageId: "unexpectedModuleExports",
fix(fixer) {
return fixModuleExports(node, sourceCode, fixer)
},
})
}
// Disallow direct assignment to `exports`.
for (const node of exportsNodes) {
// Skip if it's not assignee.
if (!isAssignee(node)) {
continue
}
// Check if it's a batch assignment.
if (batchAssignList.indexOf(getTopAssignment(node)) !== -1) {
continue
}
// Report.
context.report({
node,
loc: getLocation(node),
messageId: "unexpectedAssignment",
})
}
}
return {
"Program:exit"(node) {
const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
switch (mode) {
case "module.exports":
enforceModuleExports(scope)
break
case "exports":
enforceExports(scope)
break
// no default
}
},
}
},
}

View file

@ -0,0 +1,126 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const fs = require("fs")
const mapTypescriptExtension = require("../util/map-typescript-extension")
const visitImport = require("../util/visit-import")
/**
* Get all file extensions of the files which have the same basename.
* @param {string} filePath The path to the original file to check.
* @returns {string[]} File extensions.
*/
function getExistingExtensions(filePath) {
const basename = path.basename(filePath, path.extname(filePath))
try {
return fs
.readdirSync(path.dirname(filePath))
.filter(
filename =>
path.basename(filename, path.extname(filename)) === basename
)
.map(filename => path.extname(filename))
} catch (_error) {
return []
}
}
module.exports = {
meta: {
docs: {
description:
"enforce the style of file extensions in `import` declarations",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/file-extension-in-import.md",
},
fixable: "code",
messages: {
requireExt: "require file extension '{{ext}}'.",
forbidExt: "forbid file extension '{{ext}}'.",
},
schema: [
{
enum: ["always", "never"],
},
{
type: "object",
properties: {},
additionalProperties: {
enum: ["always", "never"],
},
},
],
type: "suggestion",
},
create(context) {
if ((context.filename ?? context.getFilename()).startsWith("<")) {
return {}
}
const defaultStyle = context.options[0] || "always"
const overrideStyle = context.options[1] || {}
/**
* @param {import("../util/import-target.js")} target
* @returns {void}
*/
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") {
return
}
// Get extension.
const originalExt = path.extname(name)
const existingExts = getExistingExtensions(filePath)
const ext = path.extname(filePath) || existingExts.join(" or ")
const style = overrideStyle[ext] || defaultStyle
// Verify.
if (style === "always" && ext !== originalExt) {
const fileExtensionToAdd = mapTypescriptExtension(
context,
filePath,
ext
)
context.report({
node,
messageId: "requireExt",
data: { ext: fileExtensionToAdd },
fix(fixer) {
if (existingExts.length !== 1) {
return null
}
const index = node.range[1] - 1
return fixer.insertTextBeforeRange(
[index, index],
fileExtensionToAdd
)
},
})
} else if (style === "never" && ext === originalExt) {
context.report({
node,
messageId: "forbidExt",
data: { ext },
fix(fixer) {
if (existingExts.length !== 1) {
return null
}
const index = name.lastIndexOf(ext)
const start = node.range[0] + 1 + index
const end = start + ext.length
return fixer.removeRange([start, end])
},
})
}
}
return visitImport(context, { optionIndex: 1 }, targets => {
targets.forEach(verify)
})
},
}

View file

@ -0,0 +1,94 @@
/**
* @author Jamund Ferguson
* See LICENSE file in root directory for full license.
*/
"use strict"
const ACCEPTABLE_PARENTS = [
"AssignmentExpression",
"VariableDeclarator",
"MemberExpression",
"ExpressionStatement",
"CallExpression",
"ConditionalExpression",
"Program",
"VariableDeclaration",
]
/**
* 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.
*/
function findReference(scope, node) {
const references = scope.references.filter(
reference =>
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.
* @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
}
module.exports = {
meta: {
type: "suggestion",
docs: {
description:
"require `require()` calls to be placed at top-level module scope",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/global-require.md",
},
fixable: null,
schema: [],
messages: {
unexpected: "Unexpected require().",
},
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
return {
CallExpression(node) {
const currentScope =
sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
if (
node.callee.name === "require" &&
!isShadowed(currentScope, node.callee)
) {
const isGoodRequire = (
sourceCode.getAncestors?.(node) ??
context.getAncestors()
) // TODO: remove context.getAncestors() when dropping support for ESLint < v9
.every(
parent =>
ACCEPTABLE_PARENTS.indexOf(parent.type) > -1
)
if (!isGoodRequire) {
context.report({ node, messageId: "unexpected" })
}
}
},
}
},
}

View file

@ -0,0 +1,93 @@
/**
* @author Jamund Ferguson
* See LICENSE file in root directory for full license.
*/
"use strict"
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "require error handling in callbacks",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/handle-callback-err.md",
},
fixable: null,
schema: [
{
type: "string",
},
],
messages: {
expected: "Expected error to be handled.",
},
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const errorArgument = context.options[0] || "err"
/**
* Checks if the given argument should be interpreted as a regexp pattern.
* @param {string} stringToCheck The string which should be checked.
* @returns {boolean} Whether or not the string should be interpreted as a pattern.
*/
function isPattern(stringToCheck) {
const firstChar = stringToCheck[0]
return firstChar === "^"
}
/**
* Checks if the given name matches the configured error argument.
* @param {string} name The name which should be compared.
* @returns {boolean} Whether or not the given name matches the configured error variable name.
*/
function matchesConfiguredErrorName(name) {
if (isPattern(errorArgument)) {
const regexp = new RegExp(errorArgument, "u")
return regexp.test(name)
}
return name === errorArgument
}
/**
* Get the parameters of a given function scope.
* @param {Object} scope The function scope.
* @returns {Array} All parameters of the given scope.
*/
function getParameters(scope) {
return scope.variables.filter(
variable =>
variable.defs[0] && variable.defs[0].type === "Parameter"
)
}
/**
* Check to see if we're handling the error object properly.
* @param {ASTNode} node The AST node to check.
* @returns {void}
*/
function checkForError(node) {
const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
const parameters = getParameters(scope)
const firstParameter = parameters[0]
if (
firstParameter &&
matchesConfiguredErrorName(firstParameter.name)
) {
if (firstParameter.references.length === 0) {
context.report({ node, messageId: "expected" })
}
}
}
return {
FunctionDeclaration: checkForError,
FunctionExpression: checkForError,
ArrowFunctionExpression: checkForError,
}
},
}

View file

@ -0,0 +1,83 @@
/**
* @author Jamund Ferguson
* See LICENSE file in root directory for full license.
*/
"use strict"
module.exports = {
meta: {
docs: {
description:
"enforce Node.js-style error-first callback pattern is followed",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-callback-literal.md",
},
type: "problem",
fixable: null,
schema: [],
messages: {
unexpectedLiteral:
"Unexpected literal in error position of callback.",
},
},
create(context) {
const callbackNames = ["callback", "cb"]
function isCallback(name) {
return callbackNames.indexOf(name) > -1
}
return {
CallExpression(node) {
const errorArg = node.arguments[0]
const calleeName = node.callee.name
if (
errorArg &&
!couldBeError(errorArg) &&
isCallback(calleeName)
) {
context.report({
node,
messageId: "unexpectedLiteral",
})
}
},
}
},
}
/**
* Determine if a node has a possiblity to be an Error object
* @param {ASTNode} node ASTNode to check
* @returns {boolean} True if there is a chance it contains an Error obj
*/
function couldBeError(node) {
switch (node.type) {
case "Identifier":
case "CallExpression":
case "NewExpression":
case "MemberExpression":
case "TaggedTemplateExpression":
case "YieldExpression":
return true // possibly an error object.
case "Literal":
return node.value == null
case "AssignmentExpression":
return couldBeError(node.right)
case "SequenceExpression": {
const exprs = node.expressions
return exprs.length !== 0 && couldBeError(exprs[exprs.length - 1])
}
case "LogicalExpression":
return couldBeError(node.left) || couldBeError(node.right)
case "ConditionalExpression":
return couldBeError(node.consequent) || couldBeError(node.alternate)
default:
return true // assuming unknown nodes can be error objects.
}
}

View file

@ -0,0 +1,791 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const {
CALL,
CONSTRUCT,
READ,
ReferenceTracker,
} = require("@eslint-community/eslint-utils")
const enumeratePropertyNames = require("../util/enumerate-property-names")
const getConfiguredNodeVersion = require("../util/get-configured-node-version")
const getSemverRange = require("../util/get-semver-range")
const extendTrackmapWithNodePrefix = require("../util/extend-trackmap-with-node-prefix")
const unprefixNodeColon = require("../util/unprefix-node-colon")
const rawModules = {
_linklist: {
[READ]: { since: "5.0.0", replacedBy: null },
},
_stream_wrap: {
[READ]: { since: "12.0.0", replacedBy: null },
},
async_hooks: {
currentId: {
[READ]: {
since: "8.2.0",
replacedBy: [
{
name: "'async_hooks.executionAsyncId()'",
supported: "8.1.0",
},
],
},
},
triggerId: {
[READ]: {
since: "8.2.0",
replacedBy: "'async_hooks.triggerAsyncId()'",
},
},
},
buffer: {
Buffer: {
[CONSTRUCT]: {
since: "6.0.0",
replacedBy: [
{ name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
{ name: "'buffer.Buffer.from()'", supported: "5.10.0" },
],
},
[CALL]: {
since: "6.0.0",
replacedBy: [
{ name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
{ name: "'buffer.Buffer.from()'", supported: "5.10.0" },
],
},
},
SlowBuffer: {
[READ]: {
since: "6.0.0",
replacedBy: [
{
name: "'buffer.Buffer.allocUnsafeSlow()'",
supported: "5.12.0",
},
],
},
},
},
constants: {
[READ]: {
since: "6.3.0",
replacedBy: "'constants' property of each module",
},
},
crypto: {
_toBuf: {
[READ]: { since: "11.0.0", replacedBy: null },
},
Credentials: {
[READ]: { since: "0.12.0", replacedBy: "'tls.SecureContext'" },
},
DEFAULT_ENCODING: {
[READ]: { since: "10.0.0", replacedBy: null },
},
createCipher: {
[READ]: {
since: "10.0.0",
replacedBy: [
{ name: "'crypto.createCipheriv()'", supported: "0.1.94" },
],
},
},
createCredentials: {
[READ]: {
since: "0.12.0",
replacedBy: [
{
name: "'tls.createSecureContext()'",
supported: "0.11.13",
},
],
},
},
createDecipher: {
[READ]: {
since: "10.0.0",
replacedBy: [
{
name: "'crypto.createDecipheriv()'",
supported: "0.1.94",
},
],
},
},
fips: {
[READ]: {
since: "10.0.0",
replacedBy: [
{
name: "'crypto.getFips()' and 'crypto.setFips()'",
supported: "10.0.0",
},
],
},
},
prng: {
[READ]: {
since: "11.0.0",
replacedBy: [
{ name: "'crypto.randomBytes()'", supported: "0.5.8" },
],
},
},
pseudoRandomBytes: {
[READ]: {
since: "11.0.0",
replacedBy: [
{ name: "'crypto.randomBytes()'", supported: "0.5.8" },
],
},
},
rng: {
[READ]: {
since: "11.0.0",
replacedBy: [
{ name: "'crypto.randomBytes()'", supported: "0.5.8" },
],
},
},
},
domain: {
[READ]: { since: "4.0.0", replacedBy: null },
},
events: {
EventEmitter: {
listenerCount: {
[READ]: {
since: "4.0.0",
replacedBy: [
{
name: "'events.EventEmitter#listenerCount()'",
supported: "3.2.0",
},
],
},
},
},
listenerCount: {
[READ]: {
since: "4.0.0",
replacedBy: [
{
name: "'events.EventEmitter#listenerCount()'",
supported: "3.2.0",
},
],
},
},
},
freelist: {
[READ]: { since: "4.0.0", replacedBy: null },
},
fs: {
SyncWriteStream: {
[READ]: { since: "4.0.0", replacedBy: null },
},
exists: {
[READ]: {
since: "4.0.0",
replacedBy: [
{ name: "'fs.stat()'", supported: "0.0.2" },
{ name: "'fs.access()'", supported: "0.11.15" },
],
},
},
lchmod: {
[READ]: { since: "0.4.0", replacedBy: null },
},
lchmodSync: {
[READ]: { since: "0.4.0", replacedBy: null },
},
},
http: {
createClient: {
[READ]: {
since: "0.10.0",
replacedBy: [{ name: "'http.request()'", supported: "0.3.6" }],
},
},
},
module: {
Module: {
createRequireFromPath: {
[READ]: {
since: "12.2.0",
replacedBy: [
{
name: "'module.createRequire()'",
supported: "12.2.0",
},
],
},
},
requireRepl: {
[READ]: {
since: "6.0.0",
replacedBy: "'require(\"repl\")'",
},
},
_debug: {
[READ]: { since: "9.0.0", replacedBy: null },
},
},
createRequireFromPath: {
[READ]: {
since: "12.2.0",
replacedBy: [
{
name: "'module.createRequire()'",
supported: "12.2.0",
},
],
},
},
requireRepl: {
[READ]: {
since: "6.0.0",
replacedBy: "'require(\"repl\")'",
},
},
_debug: {
[READ]: { since: "9.0.0", replacedBy: null },
},
},
net: {
_setSimultaneousAccepts: {
[READ]: { since: "12.0.0", replacedBy: null },
},
},
os: {
getNetworkInterfaces: {
[READ]: {
since: "0.6.0",
replacedBy: [
{ name: "'os.networkInterfaces()'", supported: "0.6.0" },
],
},
},
tmpDir: {
[READ]: {
since: "7.0.0",
replacedBy: [{ name: "'os.tmpdir()'", supported: "0.9.9" }],
},
},
},
path: {
_makeLong: {
[READ]: {
since: "9.0.0",
replacedBy: [
{ name: "'path.toNamespacedPath()'", supported: "9.0.0" },
],
},
},
},
process: {
EventEmitter: {
[READ]: {
since: "0.6.0",
replacedBy: "'require(\"events\")'",
},
},
assert: {
[READ]: {
since: "10.0.0",
replacedBy: "'require(\"assert\")'",
},
},
binding: {
[READ]: { since: "10.9.0", replacedBy: null },
},
env: {
NODE_REPL_HISTORY_FILE: {
[READ]: {
since: "4.0.0",
replacedBy: "'NODE_REPL_HISTORY'",
},
},
},
report: {
triggerReport: {
[READ]: {
since: "11.12.0",
replacedBy: "'process.report.writeReport()'",
},
},
},
},
punycode: {
[READ]: {
since: "7.0.0",
replacedBy: "'https://www.npmjs.com/package/punycode'",
},
},
readline: {
codePointAt: {
[READ]: { since: "4.0.0", replacedBy: null },
},
getStringWidth: {
[READ]: { since: "6.0.0", replacedBy: null },
},
isFullWidthCodePoint: {
[READ]: { since: "6.0.0", replacedBy: null },
},
stripVTControlCharacters: {
[READ]: { since: "6.0.0", replacedBy: null },
},
},
// safe-buffer.Buffer function/constructror is just a re-export of buffer.Buffer
// and should be deprecated likewise.
"safe-buffer": {
Buffer: {
[CONSTRUCT]: {
since: "6.0.0",
replacedBy: [
{ name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
{ name: "'buffer.Buffer.from()'", supported: "5.10.0" },
],
},
[CALL]: {
since: "6.0.0",
replacedBy: [
{ name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
{ name: "'buffer.Buffer.from()'", supported: "5.10.0" },
],
},
},
SlowBuffer: {
[READ]: {
since: "6.0.0",
replacedBy: [
{
name: "'buffer.Buffer.allocUnsafeSlow()'",
supported: "5.12.0",
},
],
},
},
},
sys: {
[READ]: {
since: "0.3.0",
replacedBy: "'util' module",
},
},
timers: {
enroll: {
[READ]: {
since: "10.0.0",
replacedBy: [
{ name: "'setTimeout()'", supported: "0.0.1" },
{ name: "'setInterval()'", supported: "0.0.1" },
],
},
},
unenroll: {
[READ]: {
since: "10.0.0",
replacedBy: [
{ name: "'clearTimeout()'", supported: "0.0.1" },
{ name: "'clearInterval()'", supported: "0.0.1" },
],
},
},
},
tls: {
CleartextStream: {
[READ]: { since: "0.10.0", replacedBy: null },
},
CryptoStream: {
[READ]: {
since: "0.12.0",
replacedBy: [{ name: "'tls.TLSSocket'", supported: "0.11.4" }],
},
},
SecurePair: {
[READ]: {
since: "6.0.0",
replacedBy: [{ name: "'tls.TLSSocket'", supported: "0.11.4" }],
},
},
convertNPNProtocols: {
[READ]: { since: "10.0.0", replacedBy: null },
},
createSecurePair: {
[READ]: {
since: "6.0.0",
replacedBy: [{ name: "'tls.TLSSocket'", supported: "0.11.4" }],
},
},
parseCertString: {
[READ]: {
since: "8.6.0",
replacedBy: [
{ name: "'querystring.parse()'", supported: "0.1.25" },
],
},
},
},
tty: {
setRawMode: {
[READ]: {
since: "0.10.0",
replacedBy:
"'tty.ReadStream#setRawMode()' (e.g. 'process.stdin.setRawMode()')",
},
},
},
url: {
parse: {
[READ]: {
since: "11.0.0",
replacedBy: [
{ name: "'url.URL' constructor", supported: "6.13.0" },
],
},
},
resolve: {
[READ]: {
since: "11.0.0",
replacedBy: [
{ name: "'url.URL' constructor", supported: "6.13.0" },
],
},
},
},
util: {
debug: {
[READ]: {
since: "0.12.0",
replacedBy: [
{ name: "'console.error()'", supported: "0.1.100" },
],
},
},
error: {
[READ]: {
since: "0.12.0",
replacedBy: [
{ name: "'console.error()'", supported: "0.1.100" },
],
},
},
isArray: {
[READ]: {
since: "4.0.0",
replacedBy: [
{ name: "'Array.isArray()'", supported: "0.1.100" },
],
},
},
isBoolean: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isBuffer: {
[READ]: {
since: "4.0.0",
replacedBy: [
{ name: "'Buffer.isBuffer()'", supported: "0.1.101" },
],
},
},
isDate: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isError: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isFunction: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isNull: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isNullOrUndefined: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isNumber: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isObject: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isPrimitive: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isRegExp: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isString: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isSymbol: {
[READ]: { since: "4.0.0", replacedBy: null },
},
isUndefined: {
[READ]: { since: "4.0.0", replacedBy: null },
},
log: {
[READ]: { since: "6.0.0", replacedBy: "a third party module" },
},
print: {
[READ]: {
since: "0.12.0",
replacedBy: [{ name: "'console.log()'", supported: "0.1.100" }],
},
},
pump: {
[READ]: {
since: "0.10.0",
replacedBy: [
{ name: "'stream.Readable#pipe()'", supported: "0.9.4" },
],
},
},
puts: {
[READ]: {
since: "0.12.0",
replacedBy: [{ name: "'console.log()'", supported: "0.1.100" }],
},
},
_extend: {
[READ]: {
since: "6.0.0",
replacedBy: [{ name: "'Object.assign()'", supported: "4.0.0" }],
},
},
},
vm: {
runInDebugContext: {
[READ]: { since: "8.0.0", replacedBy: null },
},
},
}
const modules = extendTrackmapWithNodePrefix(rawModules)
const globals = {
Buffer: {
[CONSTRUCT]: {
since: "6.0.0",
replacedBy: [
{ name: "'Buffer.alloc()'", supported: "5.10.0" },
{ name: "'Buffer.from()'", supported: "5.10.0" },
],
},
[CALL]: {
since: "6.0.0",
replacedBy: [
{ name: "'Buffer.alloc()'", supported: "5.10.0" },
{ name: "'Buffer.from()'", supported: "5.10.0" },
],
},
},
COUNTER_NET_SERVER_CONNECTION: {
[READ]: { since: "11.0.0", replacedBy: null },
},
COUNTER_NET_SERVER_CONNECTION_CLOSE: {
[READ]: { since: "11.0.0", replacedBy: null },
},
COUNTER_HTTP_SERVER_REQUEST: {
[READ]: { since: "11.0.0", replacedBy: null },
},
COUNTER_HTTP_SERVER_RESPONSE: {
[READ]: { since: "11.0.0", replacedBy: null },
},
COUNTER_HTTP_CLIENT_REQUEST: {
[READ]: { since: "11.0.0", replacedBy: null },
},
COUNTER_HTTP_CLIENT_RESPONSE: {
[READ]: { since: "11.0.0", replacedBy: null },
},
GLOBAL: {
[READ]: {
since: "6.0.0",
replacedBy: [{ name: "'global'", supported: "0.1.27" }],
},
},
Intl: {
v8BreakIterator: {
[READ]: { since: "7.0.0", replacedBy: null },
},
},
require: {
extensions: {
[READ]: {
since: "0.12.0",
replacedBy: "compiling them ahead of time",
},
},
},
root: {
[READ]: {
since: "6.0.0",
replacedBy: [{ name: "'global'", supported: "0.1.27" }],
},
},
process: modules.process,
}
/**
* Makes a replacement message.
*
* @param {string|array|null} replacedBy - The text of substitute way.
* @param {Range} version - The configured version range
* @returns {string} Replacement message.
*/
function toReplaceMessage(replacedBy, version) {
let message = replacedBy
if (Array.isArray(replacedBy)) {
message = replacedBy
.filter(
({ supported }) =>
!version.intersects(getSemverRange(`<${supported}`))
)
.map(({ name }) => name)
.join(" or ")
}
return message ? `. Use ${message} instead` : ""
}
/**
* Convert a given path to name.
* @param {symbol} type The report type.
* @param {string[]} path The property access path.
* @returns {string} The name.
*/
function toName(type, path) {
const baseName = unprefixNodeColon(path.join("."))
return type === ReferenceTracker.CALL
? `${baseName}()`
: type === ReferenceTracker.CONSTRUCT
? `new ${baseName}()`
: baseName
}
/**
* Parses the options.
* @param {RuleContext} context The rule context.
* @returns {{version:Range,ignoredGlobalItems:Set<string>,ignoredModuleItems:Set<string>}} Parsed
* value.
*/
function parseOptions(context) {
const raw = context.options[0] || {}
const version = getConfiguredNodeVersion(context)
const ignoredModuleItems = new Set(raw.ignoreModuleItems || [])
const ignoredGlobalItems = new Set(raw.ignoreGlobalItems || [])
return Object.freeze({ version, ignoredGlobalItems, ignoredModuleItems })
}
module.exports = {
meta: {
docs: {
description: "disallow deprecated APIs",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-deprecated-api.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
version: getConfiguredNodeVersion.schema,
ignoreModuleItems: {
type: "array",
items: {
enum: Array.from(
enumeratePropertyNames(rawModules)
),
},
additionalItems: false,
uniqueItems: true,
},
ignoreGlobalItems: {
type: "array",
items: {
enum: Array.from(enumeratePropertyNames(globals)),
},
additionalItems: false,
uniqueItems: true,
},
// Deprecated since v4.2.0
ignoreIndirectDependencies: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
deprecated:
"{{name}} was deprecated since v{{version}}{{replace}}.",
},
},
create(context) {
const { ignoredModuleItems, ignoredGlobalItems, version } =
parseOptions(context)
/**
* Reports a use of a deprecated API.
*
* @param {ASTNode} node - A node to report.
* @param {string} name - The name of a deprecated API.
* @param {{since: number, replacedBy: string}} info - Information of the API.
* @returns {void}
*/
function reportItem(node, name, info) {
context.report({
node,
loc: node.loc,
messageId: "deprecated",
data: {
name,
version: info.since,
replace: toReplaceMessage(info.replacedBy, version),
},
})
}
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
return {
"Program:exit"(node) {
const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
const tracker = new ReferenceTracker(scope, {
mode: "legacy",
})
for (const report of tracker.iterateGlobalReferences(globals)) {
const { node, path, type, info } = report
const name = toName(type, path)
if (!ignoredGlobalItems.has(name)) {
reportItem(node, `'${name}'`, info)
}
}
for (const report of [
...tracker.iterateCjsReferences(modules),
...tracker.iterateEsmReferences(modules),
]) {
const { node, path, type, info } = report
const name = toName(type, path)
const suffix = path.length === 1 ? " module" : ""
if (!ignoredModuleItems.has(name)) {
reportItem(node, `'${name}'${suffix}`, info)
}
}
},
}
},
}

View file

@ -0,0 +1,76 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* See LICENSE file in root directory for full license.
*/
"use strict"
const { findVariable } = require("@eslint-community/eslint-utils")
function isExports(node, scope) {
let variable = null
return (
node != null &&
node.type === "Identifier" &&
node.name === "exports" &&
(variable = findVariable(scope, node)) != null &&
variable.scope.type === "global"
)
}
function isModuleExports(node, scope) {
let variable = null
return (
node != null &&
node.type === "MemberExpression" &&
!node.computed &&
node.object.type === "Identifier" &&
node.object.name === "module" &&
node.property.type === "Identifier" &&
node.property.name === "exports" &&
(variable = findVariable(scope, node.object)) != null &&
variable.scope.type === "global"
)
}
module.exports = {
meta: {
docs: {
description: "disallow the assignment to `exports`",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-exports-assign.md",
},
fixable: null,
messages: {
forbidden:
"Unexpected assignment to 'exports' variable. Use 'module.exports' instead.",
},
schema: [],
type: "problem",
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
return {
AssignmentExpression(node) {
const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
if (
!isExports(node.left, scope) ||
// module.exports = exports = {}
(node.parent.type === "AssignmentExpression" &&
node.parent.right === node &&
isModuleExports(node.parent.left, scope)) ||
// exports = module.exports = {}
(node.right.type === "AssignmentExpression" &&
isModuleExports(node.right.left, scope))
) {
return
}
context.report({ node, messageId: "forbidden" })
},
}
},
}

View file

@ -0,0 +1,46 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkExtraneous, messages } = require("../util/check-extraneous")
const getAllowModules = require("../util/get-allow-modules")
const getConvertPath = require("../util/get-convert-path")
const getResolvePaths = require("../util/get-resolve-paths")
const visitImport = require("../util/visit-import")
module.exports = {
meta: {
docs: {
description:
"disallow `import` declarations which import extraneous modules",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-extraneous-import.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
allowModules: getAllowModules.schema,
convertPath: getConvertPath.schema,
resolvePaths: getResolvePaths.schema,
},
additionalProperties: false,
},
],
messages,
},
create(context) {
const filePath = context.filename ?? context.getFilename()
if (filePath === "<input>") {
return {}
}
return visitImport(context, {}, targets => {
checkExtraneous(context, filePath, targets)
})
},
}

View file

@ -0,0 +1,48 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkExtraneous, messages } = require("../util/check-extraneous")
const getAllowModules = require("../util/get-allow-modules")
const getConvertPath = require("../util/get-convert-path")
const getResolvePaths = require("../util/get-resolve-paths")
const getTryExtensions = require("../util/get-try-extensions")
const visitRequire = require("../util/visit-require")
module.exports = {
meta: {
docs: {
description:
"disallow `require()` expressions which import extraneous modules",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-extraneous-require.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
allowModules: getAllowModules.schema,
convertPath: getConvertPath.schema,
resolvePaths: getResolvePaths.schema,
tryExtensions: getTryExtensions.schema,
},
additionalProperties: false,
},
],
messages,
},
create(context) {
const filePath = context.filename ?? context.getFilename()
if (filePath === "<input>") {
return {}
}
return visitRequire(context, {}, targets => {
checkExtraneous(context, filePath, targets)
})
},
}

View file

@ -0,0 +1,172 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*
* @deprecated since v4.2.0
* This rule was based on an invalid assumption.
* No meaning.
*/
"use strict"
const path = require("path")
const resolve = require("resolve")
const { pathToFileURL, fileURLToPath } = require("url")
const {
defaultResolve: importResolve,
} = require("../converted-esm/import-meta-resolve")
const getPackageJson = require("../util/get-package-json")
const mergeVisitorsInPlace = require("../util/merge-visitors-in-place")
const visitImport = require("../util/visit-import")
const visitRequire = require("../util/visit-require")
const CORE_MODULES = new Set([
"assert",
"buffer",
"child_process",
"cluster",
"console",
"constants",
"crypto",
"dgram",
"dns",
/* "domain", */ "events",
"fs",
"http",
"https",
"module",
"net",
"os",
"path",
/* "punycode", */ "querystring",
"readline",
"repl",
"stream",
"string_decoder",
"timers",
"tls",
"tty",
"url",
"util",
"vm",
"zlib",
])
const BACK_SLASH = /\\/gu
module.exports = {
meta: {
docs: {
description:
"disallow third-party modules which are hiding core modules",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-hide-core-modules.md",
},
type: "problem",
deprecated: true,
fixable: null,
schema: [
{
type: "object",
properties: {
allow: {
type: "array",
items: { enum: Array.from(CORE_MODULES) },
additionalItems: false,
uniqueItems: true,
},
ignoreDirectDependencies: { type: "boolean" },
ignoreIndirectDependencies: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
unexpectedImport:
"Unexpected import of third-party module '{{name}}'.",
},
},
create(context) {
const filename = context.filename ?? context.getFilename()
if (filename === "<input>") {
return {}
}
const filePath = path.resolve(filename)
const dirPath = path.dirname(filePath)
const packageJson = getPackageJson(filePath)
const deps = new Set(
[].concat(
Object.keys((packageJson && packageJson.dependencies) || {}),
Object.keys((packageJson && packageJson.devDependencies) || {})
)
)
const options = context.options[0] || {}
const allow = options.allow || []
const ignoreDirectDependencies = Boolean(
options.ignoreDirectDependencies
)
const ignoreIndirectDependencies = Boolean(
options.ignoreIndirectDependencies
)
const targets = []
return [
visitImport(context, { includeCore: true }, importTargets =>
targets.push(...importTargets)
),
visitRequire(context, { includeCore: true }, requireTargets =>
targets.push(...requireTargets)
),
{
"Program:exit"() {
for (const target of targets.filter(
t =>
CORE_MODULES.has(t.moduleName) &&
t.moduleName === t.name
)) {
const name = target.moduleName
const allowed =
allow.indexOf(name) !== -1 ||
(ignoreDirectDependencies && deps.has(name)) ||
(ignoreIndirectDependencies && !deps.has(name))
if (allowed) {
continue
}
let resolved = ""
const moduleId = `${name}/`
try {
resolved = resolve.sync(moduleId, {
basedir: dirPath,
})
} catch (_error) {
try {
const { url } = importResolve(moduleId, {
parentURL: pathToFileURL(dirPath).href,
})
resolved = fileURLToPath(url)
} catch (_error) {
continue
}
}
context.report({
node: target.node,
loc: target.node.loc,
messageId: "unexpectedImport",
data: {
name: path
.relative(dirPath, resolved)
.replace(BACK_SLASH, "/"),
},
})
}
},
},
].reduce(
(mergedVisitor, thisVisitor) =>
mergeVisitorsInPlace(mergedVisitor, thisVisitor),
{}
)
},
}

View file

@ -0,0 +1,48 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkExistence, messages } = require("../util/check-existence")
const getAllowModules = require("../util/get-allow-modules")
const getResolvePaths = require("../util/get-resolve-paths")
const getTSConfig = require("../util/get-tsconfig")
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
const visitImport = require("../util/visit-import")
module.exports = {
meta: {
docs: {
description:
"disallow `import` declarations which import non-existence modules",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-missing-import.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
allowModules: getAllowModules.schema,
resolvePaths: getResolvePaths.schema,
typescriptExtensionMap: getTypescriptExtensionMap.schema,
tsconfigPath: getTSConfig.schema,
},
additionalProperties: false,
},
],
messages,
},
create(context) {
const filePath = context.filename ?? context.getFilename()
if (filePath === "<input>") {
return {}
}
return visitImport(context, {}, targets => {
checkExistence(context, targets)
})
},
}

View file

@ -0,0 +1,50 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkExistence, messages } = require("../util/check-existence")
const getAllowModules = require("../util/get-allow-modules")
const getResolvePaths = require("../util/get-resolve-paths")
const getTSConfig = require("../util/get-tsconfig")
const getTryExtensions = require("../util/get-try-extensions")
const getTypescriptExtensionMap = require("../util/get-typescript-extension-map")
const visitRequire = require("../util/visit-require")
module.exports = {
meta: {
docs: {
description:
"disallow `require()` expressions which import non-existence modules",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-missing-require.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
allowModules: getAllowModules.schema,
tryExtensions: getTryExtensions.schema,
resolvePaths: getResolvePaths.schema,
typescriptExtensionMap: getTypescriptExtensionMap.schema,
tsconfigPath: getTSConfig.schema,
},
additionalProperties: false,
},
],
messages,
},
create(context) {
const filePath = context.filename ?? context.getFilename()
if (filePath === "<input>") {
return {}
}
return visitRequire(context, {}, targets => {
checkExistence(context, targets)
})
},
}

View file

@ -0,0 +1,253 @@
/**
* @author Raphael Pigulla
* See LICENSE file in root directory for full license.
*/
"use strict"
// This list is generated using:
// `require("module").builtinModules`
//
// This was last updated using Node v13.8.0.
const BUILTIN_MODULES = [
"_http_agent",
"_http_client",
"_http_common",
"_http_incoming",
"_http_outgoing",
"_http_server",
"_stream_duplex",
"_stream_passthrough",
"_stream_readable",
"_stream_transform",
"_stream_wrap",
"_stream_writable",
"_tls_common",
"_tls_wrap",
"assert",
"async_hooks",
"buffer",
"child_process",
"cluster",
"console",
"constants",
"crypto",
"dgram",
"dns",
"domain",
"events",
"fs",
"http",
"http2",
"https",
"inspector",
"module",
"net",
"os",
"path",
"perf_hooks",
"process",
"punycode",
"querystring",
"readline",
"repl",
"stream",
"string_decoder",
"sys",
"timers",
"tls",
"trace_events",
"tty",
"url",
"util",
"v8",
"vm",
"worker_threads",
"zlib",
]
module.exports = {
meta: {
type: "suggestion",
docs: {
description:
"disallow `require` calls to be mixed with regular variable declarations",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-mixed-requires.md",
},
fixable: null,
schema: [
{
oneOf: [
{
type: "boolean",
},
{
type: "object",
properties: {
grouping: {
type: "boolean",
},
allowCall: {
type: "boolean",
},
},
additionalProperties: false,
},
],
},
],
messages: {
noMixRequire: "Do not mix 'require' and other declarations.",
noMixCoreModuleFileComputed:
"Do not mix core, module, file and computed requires.",
},
},
create(context) {
const options = context.options[0]
let grouping = false
let allowCall = false
if (typeof options === "object") {
grouping = options.grouping
allowCall = options.allowCall
} else {
grouping = Boolean(options)
}
const DECL_REQUIRE = "require"
const DECL_UNINITIALIZED = "uninitialized"
const DECL_OTHER = "other"
const REQ_CORE = "core"
const REQ_FILE = "file"
const REQ_MODULE = "module"
const REQ_COMPUTED = "computed"
/**
* Determines the type of a declaration statement.
* @param {ASTNode} initExpression The init node of the VariableDeclarator.
* @returns {string} The type of declaration represented by the expression.
*/
function getDeclarationType(initExpression) {
if (!initExpression) {
// "var x;"
return DECL_UNINITIALIZED
}
if (
initExpression.type === "CallExpression" &&
initExpression.callee.type === "Identifier" &&
initExpression.callee.name === "require"
) {
// "var x = require('util');"
return DECL_REQUIRE
}
if (
allowCall &&
initExpression.type === "CallExpression" &&
initExpression.callee.type === "CallExpression"
) {
// "var x = require('diagnose')('sub-module');"
return getDeclarationType(initExpression.callee)
}
if (initExpression.type === "MemberExpression") {
// "var x = require('glob').Glob;"
return getDeclarationType(initExpression.object)
}
// "var x = 42;"
return DECL_OTHER
}
/**
* Determines the type of module that is loaded via require.
* @param {ASTNode} initExpression The init node of the VariableDeclarator.
* @returns {string} The module type.
*/
function inferModuleType(initExpression) {
if (initExpression.type === "MemberExpression") {
// "var x = require('glob').Glob;"
return inferModuleType(initExpression.object)
}
if (initExpression.arguments.length === 0) {
// "var x = require();"
return REQ_COMPUTED
}
const arg = initExpression.arguments[0]
if (arg.type !== "Literal" || typeof arg.value !== "string") {
// "var x = require(42);"
return REQ_COMPUTED
}
if (BUILTIN_MODULES.indexOf(arg.value) !== -1) {
// "var fs = require('fs');"
return REQ_CORE
}
if (/^\.{0,2}\//u.test(arg.value)) {
// "var utils = require('./utils');"
return REQ_FILE
}
// "var async = require('async');"
return REQ_MODULE
}
/**
* Check if the list of variable declarations is mixed, i.e. whether it
* contains both require and other declarations.
* @param {ASTNode} declarations The list of VariableDeclarators.
* @returns {boolean} True if the declarations are mixed, false if not.
*/
function isMixed(declarations) {
const contains = {}
for (const declaration of declarations) {
const type = getDeclarationType(declaration.init)
contains[type] = true
}
return Boolean(
contains[DECL_REQUIRE] &&
(contains[DECL_UNINITIALIZED] || contains[DECL_OTHER])
)
}
/**
* Check if all require declarations in the given list are of the same
* type.
* @param {ASTNode} declarations The list of VariableDeclarators.
* @returns {boolean} True if the declarations are grouped, false if not.
*/
function isGrouped(declarations) {
const found = {}
for (const declaration of declarations) {
if (getDeclarationType(declaration.init) === DECL_REQUIRE) {
found[inferModuleType(declaration.init)] = true
}
}
return Object.keys(found).length <= 1
}
return {
VariableDeclaration(node) {
if (isMixed(node.declarations)) {
context.report({
node,
messageId: "noMixRequire",
})
} else if (grouping && !isGrouped(node.declarations)) {
context.report({
node,
messageId: "noMixCoreModuleFileComputed",
})
}
},
}
},
}

View file

@ -0,0 +1,37 @@
/**
* @author Wil Moore III
* See LICENSE file in root directory for full license.
*/
"use strict"
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow `new` operators with calls to `require`",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-new-require.md",
},
fixable: null,
schema: [],
messages: {
noNewRequire: "Unexpected use of new with require.",
},
},
create(context) {
return {
NewExpression(node) {
if (
node.callee.type === "Identifier" &&
node.callee.name === "require"
) {
context.report({
node,
messageId: "noNewRequire",
})
}
},
}
},
}

View file

@ -0,0 +1,216 @@
/**
* @author Nicholas C. Zakas
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const {
READ,
ReferenceTracker,
getStringIfConstant,
} = require("@eslint-community/eslint-utils")
/**
* Get the first char of the specified template element.
* @param {TemplateLiteral} node The `TemplateLiteral` node to get.
* @param {number} i The number of template elements to get first char.
* @param {Set<Node>} sepNodes The nodes of `path.sep`.
* @param {import("escope").Scope} globalScope The global scope object.
* @param {string[]} outNextChars The array to collect chars.
* @returns {void}
*/
function collectFirstCharsOfTemplateElement(
node,
i,
sepNodes,
globalScope,
outNextChars
) {
const element = node.quasis[i].value.cooked
if (element == null) {
return
}
if (element !== "") {
outNextChars.push(element[0])
return
}
if (node.expressions.length > i) {
collectFirstChars(
node.expressions[i],
sepNodes,
globalScope,
outNextChars
)
}
}
/**
* Get the first char of a given node.
* @param {TemplateLiteral} node The `TemplateLiteral` node to get.
* @param {Set<Node>} sepNodes The nodes of `path.sep`.
* @param {import("escope").Scope} globalScope The global scope object.
* @param {string[]} outNextChars The array to collect chars.
* @returns {void}
*/
function collectFirstChars(node, sepNodes, globalScope, outNextChars) {
switch (node.type) {
case "AssignmentExpression":
collectFirstChars(node.right, sepNodes, globalScope, outNextChars)
break
case "BinaryExpression":
collectFirstChars(node.left, sepNodes, globalScope, outNextChars)
break
case "ConditionalExpression":
collectFirstChars(
node.consequent,
sepNodes,
globalScope,
outNextChars
)
collectFirstChars(
node.alternate,
sepNodes,
globalScope,
outNextChars
)
break
case "LogicalExpression":
collectFirstChars(node.left, sepNodes, globalScope, outNextChars)
collectFirstChars(node.right, sepNodes, globalScope, outNextChars)
break
case "SequenceExpression":
collectFirstChars(
node.expressions[node.expressions.length - 1],
sepNodes,
globalScope,
outNextChars
)
break
case "TemplateLiteral":
collectFirstCharsOfTemplateElement(
node,
0,
sepNodes,
globalScope,
outNextChars
)
break
case "Identifier":
case "MemberExpression":
if (sepNodes.has(node)) {
outNextChars.push(path.sep)
break
}
// fallthrough
default: {
const str = getStringIfConstant(node, globalScope)
if (str) {
outNextChars.push(str[0])
}
}
}
}
/**
* Check if a char is a path separator or not.
* @param {string} c The char to check.
* @returns {boolean} `true` if the char is a path separator.
*/
function isPathSeparator(c) {
return c === "/" || c === path.sep
}
/**
* Check if the given Identifier node is followed by string concatenation with a
* path separator.
* @param {Identifier} node The `__dirname` or `__filename` node to check.
* @param {Set<Node>} sepNodes The nodes of `path.sep`.
* @param {import("escope").Scope} globalScope The global scope object.
* @returns {boolean} `true` if the given Identifier node is followed by string
* concatenation with a path separator.
*/
function isConcat(node, sepNodes, globalScope) {
const parent = node.parent
const nextChars = []
if (
parent.type === "BinaryExpression" &&
parent.operator === "+" &&
parent.left === node
) {
collectFirstChars(
parent.right,
sepNodes,
globalScope,
/* out */ nextChars
)
} else if (parent.type === "TemplateLiteral") {
collectFirstCharsOfTemplateElement(
parent,
parent.expressions.indexOf(node) + 1,
sepNodes,
globalScope,
/* out */ nextChars
)
}
return nextChars.some(isPathSeparator)
}
module.exports = {
meta: {
type: "suggestion",
docs: {
description:
"disallow string concatenation with `__dirname` and `__filename`",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-path-concat.md",
},
fixable: null,
schema: [],
messages: {
usePathFunctions:
"Use path.join() or path.resolve() instead of string concatenation.",
},
},
create(context) {
return {
"Program:exit"(node) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const globalScope =
sourceCode.getScope?.(node) ?? context.getScope()
const tracker = new ReferenceTracker(globalScope)
const sepNodes = new Set()
// Collect `paht.sep` references
for (const { node } of tracker.iterateCjsReferences({
path: { sep: { [READ]: true } },
})) {
sepNodes.add(node)
}
for (const { node } of tracker.iterateEsmReferences({
path: { sep: { [READ]: true } },
})) {
sepNodes.add(node)
}
// Verify `__dirname` and `__filename`
for (const { node } of tracker.iterateGlobalReferences({
__dirname: { [READ]: true },
__filename: { [READ]: true },
})) {
if (isConcat(node, sepNodes, globalScope)) {
context.report({
node: node.parent,
messageId: "usePathFunctions",
})
}
}
},
}
},
}

View file

@ -0,0 +1,43 @@
/**
* @author Vignesh Anand
* See LICENSE file in root directory for full license.
*/
"use strict"
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow the use of `process.env`",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-process-env.md",
},
fixable: null,
schema: [],
messages: {
unexpectedProcessEnv: "Unexpected use of process.env.",
},
},
create(context) {
return {
MemberExpression(node) {
const objectName = node.object.name
const propertyName = node.property.name
if (
objectName === "process" &&
!node.computed &&
propertyName &&
propertyName === "env"
) {
context.report({ node, messageId: "unexpectedProcessEnv" })
}
},
}
},
}

View file

@ -0,0 +1,34 @@
/**
* @author Nicholas C. Zakas
* See LICENSE file in root directory for full license.
*/
"use strict"
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow the use of `process.exit()`",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-process-exit.md",
},
fixable: null,
schema: [],
messages: {
noProcessExit: "Don't use process.exit(); throw an error instead.",
},
},
create(context) {
return {
"CallExpression > MemberExpression.callee[object.name = 'process'][property.name = 'exit']"(
node
) {
context.report({
node: node.parent,
messageId: "noProcessExit",
})
},
}
},
}

View file

@ -0,0 +1,58 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkForRestriction, messages } = require("../util/check-restricted")
const visit = require("../util/visit-import")
module.exports = {
meta: {
type: "suggestion",
docs: {
description:
"disallow specified modules when loaded by `import` declarations",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-restricted-import.md",
},
fixable: null,
schema: [
{
type: "array",
items: {
anyOf: [
{ type: "string" },
{
type: "object",
properties: {
name: {
anyOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
additionalItems: false,
},
],
},
message: { type: "string" },
},
additionalProperties: false,
required: ["name"],
},
],
},
additionalItems: false,
},
],
messages,
},
create(context) {
const opts = { includeCore: true }
return visit(context, opts, targets =>
checkForRestriction(context, targets)
)
},
}

View file

@ -0,0 +1,58 @@
/**
* @author Christian Schulz
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkForRestriction, messages } = require("../util/check-restricted")
const visit = require("../util/visit-require")
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow specified modules when loaded by `require`",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-restricted-require.md",
},
fixable: null,
schema: [
{
type: "array",
items: {
anyOf: [
{ type: "string" },
{
type: "object",
properties: {
name: {
anyOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
additionalItems: false,
},
],
},
message: { type: "string" },
},
additionalProperties: false,
required: ["name"],
},
],
},
additionalItems: false,
},
],
messages,
},
create(context) {
const opts = { includeCore: true }
return visit(context, opts, targets =>
checkForRestriction(context, targets)
)
},
}

View file

@ -0,0 +1,68 @@
/**
* @author Matt DuVall<http://mattduvall.com/>
* See LICENSE file in root directory for full license.
*/
"use strict"
const allowedAtRootLevelSelector = [
// fs.readFileSync()
":function MemberExpression > Identifier[name=/Sync$/]",
// readFileSync.call(null, 'path')
":function MemberExpression > Identifier[name=/Sync$/]",
// readFileSync()
":function :not(MemberExpression) > Identifier[name=/Sync$/]",
]
const disallowedAtRootLevelSelector = [
// fs.readFileSync()
"MemberExpression > Identifier[name=/Sync$/]",
// readFileSync.call(null, 'path')
"MemberExpression > Identifier[name=/Sync$/]",
// readFileSync()
":not(MemberExpression) > Identifier[name=/Sync$/]",
]
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow synchronous methods",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-sync.md",
},
fixable: null,
schema: [
{
type: "object",
properties: {
allowAtRootLevel: {
type: "boolean",
default: false,
},
},
additionalProperties: false,
},
],
messages: {
noSync: "Unexpected sync method: '{{propertyName}}'.",
},
},
create(context) {
const selector = context.options[0]?.allowAtRootLevel
? allowedAtRootLevelSelector
: disallowedAtRootLevelSelector
return {
[selector](node) {
context.report({
node: node.parent,
messageId: "noSync",
data: {
propertyName: node.name,
},
})
},
}
},
}

View file

@ -0,0 +1,98 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const getConvertPath = require("../util/get-convert-path")
const getNpmignore = require("../util/get-npmignore")
const getPackageJson = require("../util/get-package-json")
/**
* Checks whether or not a given path is a `bin` file.
*
* @param {string} filePath - A file path to check.
* @param {string|object|undefined} binField - A value of the `bin` field of `package.json`.
* @param {string} basedir - A directory path that `package.json` exists.
* @returns {boolean} `true` if the file is a `bin` file.
*/
function isBinFile(filePath, binField, basedir) {
if (!binField) {
return false
}
if (typeof binField === "string") {
return filePath === path.resolve(basedir, binField)
}
return Object.keys(binField).some(
key => filePath === path.resolve(basedir, binField[key])
)
}
module.exports = {
meta: {
docs: {
description: "disallow `bin` files that npm ignores",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-unpublished-bin.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
//
convertPath: getConvertPath.schema,
},
},
],
messages: {
invalidIgnored:
"npm ignores '{{name}}'. Check 'files' field of 'package.json' or '.npmignore'.",
},
},
create(context) {
return {
Program(node) {
// Check file path.
let rawFilePath = context.filename ?? context.getFilename()
if (rawFilePath === "<input>") {
return
}
rawFilePath = path.resolve(rawFilePath)
// Find package.json
const p = getPackageJson(rawFilePath)
if (!p) {
return
}
// Convert by convertPath option
const basedir = path.dirname(p.filePath)
const relativePath = getConvertPath(context)(
path.relative(basedir, rawFilePath).replace(/\\/gu, "/")
)
const filePath = path.join(basedir, relativePath)
// Check this file is bin.
if (!isBinFile(filePath, p.bin, basedir)) {
return
}
// Check ignored or not
const npmignore = getNpmignore(filePath)
if (!npmignore.match(relativePath)) {
return
}
// Report.
context.report({
node,
messageId: "invalidIgnored",
data: { name: relativePath },
})
},
}
},
}

View file

@ -0,0 +1,53 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkPublish, messages } = require("../util/check-publish")
const getAllowModules = require("../util/get-allow-modules")
const getConvertPath = require("../util/get-convert-path")
const getResolvePaths = require("../util/get-resolve-paths")
const visitImport = require("../util/visit-import")
module.exports = {
meta: {
docs: {
description:
"disallow `import` declarations which import private modules",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-unpublished-import.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
allowModules: getAllowModules.schema,
convertPath: getConvertPath.schema,
resolvePaths: getResolvePaths.schema,
ignoreTypeImport: { type: "boolean", default: false },
},
additionalProperties: false,
},
],
messages,
},
create(context) {
const filePath = context.filename ?? context.getFilename()
const options = context.options[0] || {}
const ignoreTypeImport =
options.ignoreTypeImport === void 0
? false
: options.ignoreTypeImport
if (filePath === "<input>") {
return {}
}
return visitImport(context, { ignoreTypeImport }, targets => {
checkPublish(context, filePath, targets)
})
},
}

View file

@ -0,0 +1,48 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { checkPublish, messages } = require("../util/check-publish")
const getAllowModules = require("../util/get-allow-modules")
const getConvertPath = require("../util/get-convert-path")
const getResolvePaths = require("../util/get-resolve-paths")
const getTryExtensions = require("../util/get-try-extensions")
const visitRequire = require("../util/visit-require")
module.exports = {
meta: {
docs: {
description:
"disallow `require()` expressions which import private modules",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-unpublished-require.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
allowModules: getAllowModules.schema,
convertPath: getConvertPath.schema,
resolvePaths: getResolvePaths.schema,
tryExtensions: getTryExtensions.schema,
},
additionalProperties: false,
},
],
messages,
},
create(context) {
const filePath = context.filename ?? context.getFilename()
if (filePath === "<input>") {
return {}
}
return visitRequire(context, {}, targets => {
checkPublish(context, filePath, targets)
})
},
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,188 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const {
checkUnsupportedBuiltins,
messages,
} = require("../../util/check-unsupported-builtins")
const enumeratePropertyNames = require("../../util/enumerate-property-names")
const getConfiguredNodeVersion = require("../../util/get-configured-node-version")
const trackMap = {
globals: {
AggregateError: {
[READ]: { supported: "15.0.0" },
},
Array: {
from: { [READ]: { supported: "4.0.0" } },
of: { [READ]: { supported: "4.0.0" } },
},
BigInt: {
[READ]: { supported: "10.4.0" },
},
FinalizationRegistry: {
[READ]: { supported: "14.6.0" },
},
Map: {
[READ]: { supported: "0.12.0" },
},
Math: {
acosh: { [READ]: { supported: "0.12.0" } },
asinh: { [READ]: { supported: "0.12.0" } },
atanh: { [READ]: { supported: "0.12.0" } },
cbrt: { [READ]: { supported: "0.12.0" } },
clz32: { [READ]: { supported: "0.12.0" } },
cosh: { [READ]: { supported: "0.12.0" } },
expm1: { [READ]: { supported: "0.12.0" } },
fround: { [READ]: { supported: "0.12.0" } },
hypot: { [READ]: { supported: "0.12.0" } },
imul: { [READ]: { supported: "0.12.0" } },
log10: { [READ]: { supported: "0.12.0" } },
log1p: { [READ]: { supported: "0.12.0" } },
log2: { [READ]: { supported: "0.12.0" } },
sign: { [READ]: { supported: "0.12.0" } },
sinh: { [READ]: { supported: "0.12.0" } },
tanh: { [READ]: { supported: "0.12.0" } },
trunc: { [READ]: { supported: "0.12.0" } },
},
Number: {
EPSILON: { [READ]: { supported: "0.12.0" } },
isFinite: { [READ]: { supported: "0.10.0" } },
isInteger: { [READ]: { supported: "0.12.0" } },
isNaN: { [READ]: { supported: "0.10.0" } },
isSafeInteger: { [READ]: { supported: "0.12.0" } },
MAX_SAFE_INTEGER: { [READ]: { supported: "0.12.0" } },
MIN_SAFE_INTEGER: { [READ]: { supported: "0.12.0" } },
parseFloat: { [READ]: { supported: "0.12.0" } },
parseInt: { [READ]: { supported: "0.12.0" } },
},
Object: {
assign: { [READ]: { supported: "4.0.0" } },
fromEntries: { [READ]: { supported: "12.0.0" } },
getOwnPropertySymbols: { [READ]: { supported: "0.12.0" } },
is: { [READ]: { supported: "0.10.0" } },
setPrototypeOf: { [READ]: { supported: "0.12.0" } },
values: { [READ]: { supported: "7.0.0" } },
entries: { [READ]: { supported: "7.0.0" } },
getOwnPropertyDescriptors: { [READ]: { supported: "7.0.0" } },
},
Promise: {
[READ]: { supported: "0.12.0" },
allSettled: { [READ]: { supported: "12.9.0" } },
any: { [READ]: { supported: "15.0.0" } },
},
Proxy: {
[READ]: { supported: "6.0.0" },
},
Reflect: {
[READ]: { supported: "6.0.0" },
},
Set: {
[READ]: { supported: "0.12.0" },
},
String: {
fromCodePoint: { [READ]: { supported: "4.0.0" } },
raw: { [READ]: { supported: "4.0.0" } },
},
Symbol: {
[READ]: { supported: "0.12.0" },
},
Int8Array: {
[READ]: { supported: "0.10.0" },
},
Uint8Array: {
[READ]: { supported: "0.10.0" },
},
Uint8ClampedArray: {
[READ]: { supported: "0.10.0" },
},
Int16Array: {
[READ]: { supported: "0.10.0" },
},
Uint16Array: {
[READ]: { supported: "0.10.0" },
},
Int32Array: {
[READ]: { supported: "0.10.0" },
},
Uint32Array: {
[READ]: { supported: "0.10.0" },
},
BigInt64Array: {
[READ]: { supported: "10.4.0" },
},
BigUint64Array: {
[READ]: { supported: "10.4.0" },
},
Float32Array: {
[READ]: { supported: "0.10.0" },
},
Float64Array: {
[READ]: { supported: "0.10.0" },
},
DataView: {
[READ]: { supported: "0.10.0" },
},
WeakMap: {
[READ]: { supported: "0.12.0" },
},
WeakRef: {
[READ]: { supported: "14.6.0" },
},
WeakSet: {
[READ]: { supported: "0.12.0" },
},
Atomics: {
[READ]: { supported: "8.10.0" },
},
SharedArrayBuffer: {
[READ]: { supported: "8.10.0" },
},
globalThis: {
[READ]: { supported: "12.0.0" },
},
},
}
module.exports = {
meta: {
docs: {
description:
"disallow unsupported ECMAScript built-ins on the specified version",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-unsupported-features/es-builtins.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
version: getConfiguredNodeVersion.schema,
ignores: {
type: "array",
items: {
enum: Array.from(
enumeratePropertyNames(trackMap.globals)
),
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages,
},
create(context) {
return {
"Program:exit"() {
checkUnsupportedBuiltins(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,692 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { rules: esRules } = require("eslint-plugin-es-x")
const { getInnermostScope } = require("@eslint-community/eslint-utils")
const { Range } = require("semver")
const rangeSubset = require("semver/ranges/subset")
const getConfiguredNodeVersion = require("../../util/get-configured-node-version")
const getSemverRange = require("../../util/get-semver-range")
const mergeVisitorsInPlace = require("../../util/merge-visitors-in-place")
const getOrSet = /^(?:g|s)et$/u
const features = {
//--------------------------------------------------------------------------
// ES2015
//--------------------------------------------------------------------------
arrowFunctions: {
ruleId: "no-arrow-functions",
cases: [
{
supported: "4.0.0",
messageId: "no-arrow-functions",
},
],
},
binaryNumericLiterals: {
ruleId: "no-binary-numeric-literals",
cases: [
{
supported: "4.0.0",
messageId: "no-binary-numeric-literals",
},
],
},
blockScopedFunctions: {
ruleId: "no-block-scoped-functions",
cases: [
{
supported: "6.0.0",
test: info => !info.isStrict,
messageId: "no-block-scoped-functions-sloppy",
},
{
supported: "4.0.0",
messageId: "no-block-scoped-functions-strict",
},
],
},
blockScopedVariables: {
ruleId: "no-block-scoped-variables",
cases: [
{
supported: "6.0.0",
test: info => !info.isStrict,
messageId: "no-block-scoped-variables-sloppy",
},
{
supported: "4.0.0",
messageId: "no-block-scoped-variables-strict",
},
],
},
classes: {
ruleId: "no-classes",
cases: [
{
supported: "6.0.0",
test: info => !info.isStrict,
messageId: "no-classes-sloppy",
},
{
supported: "4.0.0",
messageId: "no-classes-strict",
},
],
},
computedProperties: {
ruleId: "no-computed-properties",
cases: [
{
supported: "4.0.0",
messageId: "no-computed-properties",
},
],
},
defaultParameters: {
ruleId: "no-default-parameters",
cases: [
{
supported: "6.0.0",
messageId: "no-default-parameters",
},
],
},
destructuring: {
ruleId: "no-destructuring",
cases: [
{
supported: "6.0.0",
messageId: "no-destructuring",
},
],
},
forOfLoops: {
ruleId: "no-for-of-loops",
cases: [
{
supported: "0.12.0",
messageId: "no-for-of-loops",
},
],
},
generators: {
ruleId: "no-generators",
cases: [
{
supported: "4.0.0",
messageId: "no-generators",
},
],
},
modules: {
ruleId: "no-modules",
cases: [
{
supported: new Range("^12.17 || >=13.2"),
messageId: "no-modules",
},
],
},
"new.target": {
ruleId: "no-new-target",
cases: [
{
supported: "5.0.0",
messageId: "no-new-target",
},
],
},
objectSuperProperties: {
ruleId: "no-object-super-properties",
cases: [
{
supported: "4.0.0",
messageId: "no-object-super-properties",
},
],
},
octalNumericLiterals: {
ruleId: "no-octal-numeric-literals",
cases: [
{
supported: "4.0.0",
messageId: "no-octal-numeric-literals",
},
],
},
propertyShorthands: {
ruleId: "no-property-shorthands",
cases: [
{
supported: "6.0.0",
test: info =>
info.node.shorthand && getOrSet.test(info.node.key.name),
messageId: "no-property-shorthands-getset",
},
{
supported: "4.0.0",
messageId: "no-property-shorthands",
},
],
},
regexpU: {
ruleId: "no-regexp-u-flag",
cases: [
{
supported: "6.0.0",
messageId: "no-regexp-u-flag",
},
],
},
regexpY: {
ruleId: "no-regexp-y-flag",
cases: [
{
supported: "6.0.0",
messageId: "no-regexp-y-flag",
},
],
},
restParameters: {
ruleId: "no-rest-parameters",
cases: [
{
supported: "6.0.0",
messageId: "no-rest-parameters",
},
],
},
spreadElements: {
ruleId: "no-spread-elements",
cases: [
{
supported: "5.0.0",
messageId: "no-spread-elements",
},
],
},
templateLiterals: {
ruleId: "no-template-literals",
cases: [
{
supported: "4.0.0",
messageId: "no-template-literals",
},
],
},
unicodeCodePointEscapes: {
ruleId: "no-unicode-codepoint-escapes",
cases: [
{
supported: "4.0.0",
messageId: "no-unicode-codepoint-escapes",
},
],
},
//--------------------------------------------------------------------------
// ES2016
//--------------------------------------------------------------------------
exponentialOperators: {
ruleId: "no-exponential-operators",
cases: [
{
supported: "7.0.0",
messageId: "no-exponential-operators",
},
],
},
//--------------------------------------------------------------------------
// ES2017
//--------------------------------------------------------------------------
asyncFunctions: {
ruleId: "no-async-functions",
cases: [
{
supported: "7.6.0",
messageId: "no-async-functions",
},
],
},
trailingCommasInFunctions: {
ruleId: "no-trailing-function-commas",
cases: [
{
supported: "8.0.0",
messageId: "no-trailing-function-commas",
},
],
},
//--------------------------------------------------------------------------
// ES2018
//--------------------------------------------------------------------------
asyncIteration: {
ruleId: "no-async-iteration",
cases: [
{
supported: "10.0.0",
messageId: "no-async-iteration",
},
],
},
malformedTemplateLiterals: {
ruleId: "no-malformed-template-literals",
cases: [
{
supported: "8.10.0",
messageId: "no-malformed-template-literals",
},
],
},
regexpLookbehind: {
ruleId: "no-regexp-lookbehind-assertions",
cases: [
{
supported: "8.10.0",
messageId: "no-regexp-lookbehind-assertions",
},
],
},
regexpNamedCaptureGroups: {
ruleId: "no-regexp-named-capture-groups",
cases: [
{
supported: "10.0.0",
messageId: "no-regexp-named-capture-groups",
},
],
},
regexpS: {
ruleId: "no-regexp-s-flag",
cases: [
{
supported: "8.10.0",
messageId: "no-regexp-s-flag",
},
],
},
regexpUnicodeProperties: {
ruleId: "no-regexp-unicode-property-escapes",
cases: [
{
supported: "10.0.0",
messageId: "no-regexp-unicode-property-escapes",
},
],
},
restSpreadProperties: {
ruleId: "no-rest-spread-properties",
cases: [
{
supported: "8.3.0",
messageId: "no-rest-spread-properties",
},
],
},
//--------------------------------------------------------------------------
// ES2019
//--------------------------------------------------------------------------
jsonSuperset: {
ruleId: "no-json-superset",
cases: [
{
supported: "10.0.0",
messageId: "no-json-superset",
},
],
},
optionalCatchBinding: {
ruleId: "no-optional-catch-binding",
cases: [
{
supported: "10.0.0",
messageId: "no-optional-catch-binding",
},
],
},
//--------------------------------------------------------------------------
// ES2020
//--------------------------------------------------------------------------
bigint: {
ruleId: "no-bigint",
cases: [
{
supported: "10.4.0",
test: info => info.node.type === "Literal",
messageId: "no-bigint",
},
{
supported: null,
test: ({ node }) =>
node.type === "Literal" &&
(node.parent.type === "Property" ||
node.parent.type === "MethodDefinition") &&
!node.parent.computed &&
node.parent.key === node,
messageId: "no-bigint-property-names",
},
],
},
dynamicImport: {
ruleId: "no-dynamic-import",
cases: [
{
supported: new Range("^12.17 || >=13.2"),
messageId: "no-dynamic-import",
},
],
},
optionalChaining: {
ruleId: "no-optional-chaining",
cases: [
{
supported: "14.0.0",
messageId: "no-optional-chaining",
},
],
},
nullishCoalescingOperators: {
ruleId: "no-nullish-coalescing-operators",
cases: [
{
supported: "14.0.0",
messageId: "no-nullish-coalescing-operators",
},
],
},
//--------------------------------------------------------------------------
// ES2021
//--------------------------------------------------------------------------
logicalAssignmentOperators: {
ruleId: "no-logical-assignment-operators",
cases: [
{
supported: "15.0.0",
messageId: "no-logical-assignment-operators",
},
],
},
numericSeparators: {
ruleId: "no-numeric-separators",
cases: [
{
supported: "12.5.0",
messageId: "no-numeric-separators",
},
],
},
}
const keywords = Object.keys(features)
/**
* Parses the options.
* @param {RuleContext} context The rule context.
* @returns {{version:Range,ignores:Set<string>}} Parsed value.
*/
function parseOptions(context) {
const raw = context.options[0] || {}
const version = getConfiguredNodeVersion(context)
const ignores = new Set(raw.ignores || [])
return Object.freeze({ version, ignores })
}
/**
* Find the scope that a given node belongs to.
* @param {Scope} initialScope The initial scope to find.
* @param {Node} node The AST node.
* @returns {Scope} The scope that the node belongs to.
*/
function normalizeScope(initialScope, node) {
let scope = getInnermostScope(initialScope, node)
while (scope && scope.block === node) {
scope = scope.upper
}
return scope
}
/**
* Define the visitor object as merging the rules of eslint-plugin-es-x.
* @param {RuleContext} context The rule context.
* @param {{version:Range,ignores:Set<string>}} options The options.
* @returns {object} The defined visitor.
*/
function defineVisitor(context, options) {
const testInfoPrototype = {
get isStrict() {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const scope = sourceCode.getScope?.(this.node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
return normalizeScope(scope, this.node).isStrict
},
}
/**
* Check whether a given case object is full-supported on the configured node version.
* @param {{supported:string}} aCase The case object to check.
* @returns {boolean} `true` if it's supporting.
*/
function isNotSupportingVersion(aCase) {
if (!aCase.supported) {
return true
}
const supported =
typeof aCase.supported === "string"
? getSemverRange(`>=${aCase.supported}`)
: aCase.supported
return !rangeSubset(options.version, supported)
}
/**
* Define the predicate function to check whether a given case object is supported on the configured node version.
* @param {Node} node The node which is reported.
* @returns {function(aCase:{supported:string}):boolean} The predicate function.
*/
function isNotSupportingOn(node) {
return aCase =>
isNotSupportingVersion(aCase) &&
(!aCase.test || aCase.test({ node, __proto__: testInfoPrototype }))
}
return (
keywords
// Omit full-supported features and ignored features by options
// because this rule never reports those.
.filter(
keyword =>
!options.ignores.has(keyword) &&
features[keyword].cases.some(isNotSupportingVersion)
)
// Merge remaining features with overriding `context.report()`.
.reduce((visitor, keyword) => {
const { ruleId, cases } = features[keyword]
const rule = esRules[ruleId]
const thisContext = {
__proto__: context,
// Override `context.report()` then:
// - ignore if it's supported.
// - override reporting messages.
report(descriptor) {
// Set additional information.
if (descriptor.data) {
descriptor.data.version = options.version.raw
} else {
descriptor.data = { version: options.version.raw }
}
descriptor.fix = undefined
// Test and report.
const node = descriptor.node
const hitCase = cases.find(isNotSupportingOn(node))
if (hitCase) {
descriptor.messageId = hitCase.messageId
descriptor.data.supported = hitCase.supported
super.report(descriptor)
}
},
}
return mergeVisitorsInPlace(visitor, rule.create(thisContext))
}, {})
)
}
module.exports = {
meta: {
docs: {
description:
"disallow unsupported ECMAScript syntax on the specified version",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-unsupported-features/es-syntax.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
version: getConfiguredNodeVersion.schema,
ignores: {
type: "array",
items: {
enum: Object.keys(features),
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
//------------------------------------------------------------------
// ES2015
//------------------------------------------------------------------
"no-arrow-functions":
"Arrow functions are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-binary-numeric-literals":
"Binary numeric literals are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-block-scoped-functions-strict":
"Block-scoped functions in strict mode are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-block-scoped-functions-sloppy":
"Block-scoped functions in non-strict mode are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-block-scoped-variables-strict":
"Block-scoped variables in strict mode are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-block-scoped-variables-sloppy":
"Block-scoped variables in non-strict mode are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-classes-strict":
"Classes in strict mode are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-classes-sloppy":
"Classes in non-strict mode are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-computed-properties":
"Computed properties are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-default-parameters":
"Default parameters are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-destructuring":
"Destructuring is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-for-of-loops":
"'for-of' loops are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-generators":
"Generator functions are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-modules":
"Import and export declarations are not supported yet.",
"no-new-target":
"'new.target' is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-object-super-properties":
"'super' in object literals is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-octal-numeric-literals":
"Octal numeric literals are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-property-shorthands":
"Property shorthands are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-property-shorthands-getset":
"Property shorthands of 'get' and 'set' are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-regexp-u-flag":
"RegExp 'u' flag is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-regexp-y-flag":
"RegExp 'y' flag is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-rest-parameters":
"Rest parameters are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-spread-elements":
"Spread elements are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-template-literals":
"Template literals are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-unicode-codepoint-escapes":
"Unicode code point escapes are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
//------------------------------------------------------------------
// ES2016
//------------------------------------------------------------------
"no-exponential-operators":
"Exponential operators are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
//------------------------------------------------------------------
// ES2017
//------------------------------------------------------------------
"no-async-functions":
"Async functions are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-trailing-function-commas":
"Trailing commas in function syntax are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
//------------------------------------------------------------------
// ES2018
//------------------------------------------------------------------
"no-async-iteration":
"Async iteration is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-malformed-template-literals":
"Malformed template literals are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-regexp-lookbehind-assertions":
"RegExp lookbehind assertions are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-regexp-named-capture-groups":
"RegExp named capture groups are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-regexp-s-flag":
"RegExp 's' flag is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-regexp-unicode-property-escapes":
"RegExp Unicode property escapes are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-rest-spread-properties":
"Rest/spread properties are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
//------------------------------------------------------------------
// ES2019
//------------------------------------------------------------------
"no-json-superset":
"'\\u{{code}}' in string literals is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-optional-catch-binding":
"The omission of 'catch' binding is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
//------------------------------------------------------------------
// ES2020
//------------------------------------------------------------------
"no-bigint":
"Bigint literals are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-bigint-property-names":
"Bigint literal property names are not supported yet.",
"no-dynamic-import":
"'import()' expressions are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-optional-chaining":
"Optional chainings are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-nullish-coalescing-operators":
"Nullish coalescing operators are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
//------------------------------------------------------------------
// ES2021
//------------------------------------------------------------------
"no-logical-assignment-operators":
"Logical assignment operators are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
"no-numeric-separators":
"Numeric separators are not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
},
},
create(context) {
return defineVisitor(context, parseOptions(context))
},
}

View file

@ -0,0 +1,415 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const {
checkUnsupportedBuiltins,
messages,
} = require("../../util/check-unsupported-builtins")
const enumeratePropertyNames = require("../../util/enumerate-property-names")
const getConfiguredNodeVersion = require("../../util/get-configured-node-version")
const extendTrackMapWithNodePrefix = require("../../util/extend-trackmap-with-node-prefix")
const trackMap = {
globals: {
queueMicrotask: {
[READ]: { supported: "12.0.0", experimental: "11.0.0" },
},
require: {
resolve: {
paths: { [READ]: { supported: "8.9.0" } },
},
},
},
modules: {
assert: {
strict: {
[READ]: { supported: "9.9.0", backported: ["8.13.0"] },
doesNotReject: { [READ]: { supported: "10.0.0" } },
rejects: { [READ]: { supported: "10.0.0" } },
},
deepStrictEqual: { [READ]: { supported: "4.0.0" } },
doesNotReject: { [READ]: { supported: "10.0.0" } },
notDeepStrictEqual: { [READ]: { supported: "4.0.0" } },
rejects: { [READ]: { supported: "10.0.0" } },
CallTracker: {
[READ]: { supported: null, experimental: "14.2.0" },
},
},
async_hooks: {
[READ]: { supported: "8.0.0" },
createHook: { [READ]: { supported: "8.1.0" } },
AsyncLocalStorage: {
[READ]: { supported: "13.10.0", backported: ["12.17.0"] },
},
},
buffer: {
Buffer: {
alloc: { [READ]: { supported: "4.5.0" } },
allocUnsafe: { [READ]: { supported: "4.5.0" } },
allocUnsafeSlow: { [READ]: { supported: "4.5.0" } },
from: { [READ]: { supported: "4.5.0" } },
},
kMaxLength: { [READ]: { supported: "3.0.0" } },
transcode: { [READ]: { supported: "7.1.0" } },
constants: { [READ]: { supported: "8.2.0" } },
Blob: { [READ]: { supported: null, experimental: "15.7.0" } },
},
child_process: {
ChildProcess: { [READ]: { supported: "2.2.0" } },
},
console: {
clear: { [READ]: { supported: "8.3.0", backported: ["6.13.0"] } },
count: { [READ]: { supported: "8.3.0", backported: ["6.13.0"] } },
countReset: {
[READ]: { supported: "8.3.0", backported: ["6.13.0"] },
},
debug: { [READ]: { supported: "8.0.0" } },
dirxml: { [READ]: { supported: "8.0.0" } },
group: { [READ]: { supported: "8.5.0" } },
groupCollapsed: { [READ]: { supported: "8.5.0" } },
groupEnd: { [READ]: { supported: "8.5.0" } },
table: { [READ]: { supported: "10.0.0" } },
markTimeline: { [READ]: { supported: "8.0.0" } },
profile: { [READ]: { supported: "8.0.0" } },
profileEnd: { [READ]: { supported: "8.0.0" } },
timeLog: { [READ]: { supported: "10.7.0" } },
timeStamp: { [READ]: { supported: "8.0.0" } },
timeline: { [READ]: { supported: "8.0.0" } },
timelineEnd: { [READ]: { supported: "8.0.0" } },
},
crypto: {
Certificate: {
exportChallenge: { [READ]: { supported: "9.0.0" } },
exportPublicKey: { [READ]: { supported: "9.0.0" } },
verifySpkac: { [READ]: { supported: "9.0.0" } },
},
ECDH: { [READ]: { supported: "8.8.0", backported: ["6.13.0"] } },
KeyObject: { [READ]: { supported: "11.6.0" } },
createPrivateKey: { [READ]: { supported: "11.6.0" } },
createPublicKey: { [READ]: { supported: "11.6.0" } },
createSecretKey: { [READ]: { supported: "11.6.0" } },
constants: { [READ]: { supported: "6.3.0" } },
fips: { [READ]: { supported: "6.0.0" } },
generateKeyPair: { [READ]: { supported: "10.12.0" } },
generateKeyPairSync: { [READ]: { supported: "10.12.0" } },
getCurves: { [READ]: { supported: "2.3.0" } },
getFips: { [READ]: { supported: "10.0.0" } },
privateEncrypt: { [READ]: { supported: "1.1.0" } },
publicDecrypt: { [READ]: { supported: "1.1.0" } },
randomFillSync: {
[READ]: { supported: "7.10.0", backported: ["6.13.0"] },
},
randomFill: {
[READ]: { supported: "7.10.0", backported: ["6.13.0"] },
},
scrypt: { [READ]: { supported: "10.5.0" } },
scryptSync: { [READ]: { supported: "10.5.0" } },
setFips: { [READ]: { supported: "10.0.0" } },
sign: { [READ]: { supported: "12.0.0" } },
timingSafeEqual: { [READ]: { supported: "6.6.0" } },
verify: { [READ]: { supported: "12.0.0" } },
},
dns: {
Resolver: { [READ]: { supported: "8.3.0" } },
resolvePtr: { [READ]: { supported: "6.0.0" } },
promises: {
[READ]: {
supported: "11.14.0",
backported: ["10.17.0"],
experimental: "10.6.0",
},
},
},
events: {
EventEmitter: {
once: {
[READ]: { supported: "11.13.0", backported: ["10.16.0"] },
},
},
once: { [READ]: { supported: "11.13.0", backported: ["10.16.0"] } },
},
fs: {
Dirent: { [READ]: { supported: "10.10.0" } },
copyFile: { [READ]: { supported: "8.5.0" } },
copyFileSync: { [READ]: { supported: "8.5.0" } },
mkdtemp: { [READ]: { supported: "5.10.0" } },
mkdtempSync: { [READ]: { supported: "5.10.0" } },
realpath: {
native: { [READ]: { supported: "9.2.0" } },
},
realpathSync: {
native: { [READ]: { supported: "9.2.0" } },
},
promises: {
[READ]: {
supported: "11.14.0",
backported: ["10.17.0"],
experimental: "10.1.0",
},
},
writev: { [READ]: { supported: "12.9.0" } },
writevSync: { [READ]: { supported: "12.9.0" } },
readv: {
[READ]: { supported: "13.13.0", backported: ["12.17.0"] },
},
readvSync: {
[READ]: { supported: "13.13.0", backported: ["12.17.0"] },
},
lutimes: {
[READ]: { supported: "14.5.0", backported: ["12.19.0"] },
},
lutimesSync: {
[READ]: { supported: "14.5.0", backported: ["12.19.0"] },
},
opendir: {
[READ]: { supported: "12.12.0" },
},
opendirSync: {
[READ]: { supported: "12.12.0" },
},
rm: {
[READ]: { supported: "14.14.0" },
},
rmSync: {
[READ]: { supported: "14.14.0" },
},
read: {
[READ]: { supported: "13.11.0", backported: ["12.17.0"] },
},
readSync: {
[READ]: { supported: "13.11.0", backported: ["12.17.0"] },
},
Dir: {
[READ]: { supported: "12.12.0" },
},
StatWatcher: {
[READ]: { supported: "14.3.0", backported: ["12.20.0"] },
},
},
"fs/promises": {
[READ]: {
supported: "14.0.0",
},
},
http2: {
[READ]: {
supported: "10.10.0",
backported: ["8.13.0"],
experimental: "8.4.0",
},
},
inspector: {
[READ]: { supported: null, experimental: "8.0.0" },
},
module: {
Module: {
builtinModules: {
[READ]: {
supported: "9.3.0",
backported: ["6.13.0", "8.10.0"],
},
},
createRequireFromPath: { [READ]: { supported: "10.12.0" } },
createRequire: { [READ]: { supported: "12.2.0" } },
syncBuiltinESMExports: { [READ]: { supported: "12.12.0" } },
},
builtinModules: {
[READ]: {
supported: "9.3.0",
backported: ["6.13.0", "8.10.0"],
},
},
createRequireFromPath: { [READ]: { supported: "10.12.0" } },
createRequire: { [READ]: { supported: "12.2.0" } },
syncBuiltinESMExports: { [READ]: { supported: "12.12.0" } },
},
os: {
constants: {
[READ]: { supported: "6.3.0" },
priority: { [READ]: { supported: "10.10.0" } },
},
getPriority: { [READ]: { supported: "10.10.0" } },
homedir: { [READ]: { supported: "2.3.0" } },
setPriority: { [READ]: { supported: "10.10.0" } },
userInfo: { [READ]: { supported: "6.0.0" } },
},
path: {
toNamespacedPath: { [READ]: { supported: "9.0.0" } },
},
perf_hooks: {
[READ]: { supported: "8.5.0" },
monitorEventLoopDelay: { [READ]: { supported: "11.10.0" } },
},
process: {
allowedNodeEnvironmentFlags: { [READ]: { supported: "10.10.0" } },
argv0: { [READ]: { supported: "6.4.0" } },
channel: { [READ]: { supported: "7.1.0" } },
cpuUsage: { [READ]: { supported: "6.1.0" } },
emitWarning: { [READ]: { supported: "6.0.0" } },
getegid: { [READ]: { supported: "2.0.0" } },
geteuid: { [READ]: { supported: "2.0.0" } },
hasUncaughtExceptionCaptureCallback: {
[READ]: { supported: "9.3.0" },
},
hrtime: {
bigint: { [READ]: { supported: "10.7.0" } },
},
ppid: {
[READ]: {
supported: "9.2.0",
backported: ["6.13.0", "8.10.0"],
},
},
release: { [READ]: { supported: "3.0.0" } },
report: { [READ]: { supported: "14.0.0", experimental: "11.8.0" } },
resourceUsage: { [READ]: { supported: "12.6.0" } },
setegid: { [READ]: { supported: "2.0.0" } },
seteuid: { [READ]: { supported: "2.0.0" } },
setUncaughtExceptionCaptureCallback: {
[READ]: { supported: "9.3.0" },
},
stdout: {
getColorDepth: { [READ]: { supported: "9.9.0" } },
hasColor: { [READ]: { supported: "11.13.0" } },
},
stderr: {
getColorDepth: { [READ]: { supported: "9.9.0" } },
hasColor: { [READ]: { supported: "11.13.0" } },
},
},
stream: {
Readable: {
from: {
[READ]: { supported: "12.3.0", backported: ["10.17.0"] },
},
},
finished: { [READ]: { supported: "10.0.0" } },
pipeline: { [READ]: { supported: "10.0.0" } },
},
trace_events: {
[READ]: { supported: "10.0.0" },
},
url: {
URL: { [READ]: { supported: "7.0.0", backported: ["6.13.0"] } },
URLSearchParams: {
[READ]: { supported: "7.5.0", backported: ["6.13.0"] },
},
domainToASCII: { [READ]: { supported: "7.4.0" } },
domainToUnicode: { [READ]: { supported: "7.4.0" } },
},
util: {
callbackify: { [READ]: { supported: "8.2.0" } },
formatWithOptions: { [READ]: { supported: "10.0.0" } },
getSystemErrorName: {
[READ]: { supported: "9.7.0", backported: ["8.12.0"] },
},
inspect: {
custom: { [READ]: { supported: "6.6.0" } },
defaultOptions: { [READ]: { supported: "6.4.0" } },
replDefaults: { [READ]: { supported: "11.12.0" } },
},
isDeepStrictEqual: { [READ]: { supported: "9.0.0" } },
promisify: { [READ]: { supported: "8.0.0" } },
TextDecoder: {
[READ]: { supported: "8.9.0", experimental: "8.3.0" },
},
TextEncoder: {
[READ]: { supported: "8.9.0", experimental: "8.3.0" },
},
types: {
[READ]: { supported: "10.0.0" },
isBoxedPrimitive: { [READ]: { supported: "10.11.0" } },
},
},
v8: {
[READ]: { supported: "1.0.0" },
DefaultDeserializer: { [READ]: { supported: "8.0.0" } },
DefaultSerializer: { [READ]: { supported: "8.0.0" } },
Deserializer: { [READ]: { supported: "8.0.0" } },
Serializer: { [READ]: { supported: "8.0.0" } },
cachedDataVersionTag: { [READ]: { supported: "8.0.0" } },
deserialize: { [READ]: { supported: "8.0.0" } },
getHeapCodeStatistics: { [READ]: { supported: "12.8.0" } },
getHeapSnapshot: { [READ]: { supported: "11.13.0" } },
getHeapSpaceStatistics: { [READ]: { supported: "6.0.0" } },
serialize: { [READ]: { supported: "8.0.0" } },
writeHeapSnapshot: { [READ]: { supported: "11.13.0" } },
},
vm: {
Module: { [READ]: { supported: "9.6.0" } },
compileFunction: { [READ]: { supported: "10.10.0" } },
},
worker_threads: {
[READ]: { supported: "12.11.0", experimental: "10.5.0" },
},
},
}
Object.assign(trackMap.globals, {
Buffer: trackMap.modules.buffer.Buffer,
TextDecoder: {
...trackMap.modules.util.TextDecoder,
[READ]: { supported: "11.0.0" },
},
TextEncoder: {
...trackMap.modules.util.TextEncoder,
[READ]: { supported: "11.0.0" },
},
URL: {
...trackMap.modules.url.URL,
[READ]: { supported: "10.0.0" },
},
URLSearchParams: {
...trackMap.modules.url.URLSearchParams,
[READ]: { supported: "10.0.0" },
},
console: trackMap.modules.console,
process: trackMap.modules.process,
})
trackMap.modules = extendTrackMapWithNodePrefix(trackMap.modules)
module.exports = {
meta: {
docs: {
description:
"disallow unsupported Node.js built-in APIs on the specified version",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/no-unsupported-features/node-builtins.md",
},
type: "problem",
fixable: null,
schema: [
{
type: "object",
properties: {
version: getConfiguredNodeVersion.schema,
ignores: {
type: "array",
items: {
enum: Array.from(
new Set([
...enumeratePropertyNames(trackMap.globals),
...enumeratePropertyNames(trackMap.modules),
])
),
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages,
},
create(context) {
return {
"Program:exit"() {
checkUnsupportedBuiltins(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,47 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const checkForPreferGlobal = require("../../util/check-prefer-global")
const trackMap = {
globals: {
Buffer: { [READ]: true },
},
modules: {
buffer: {
Buffer: { [READ]: true },
},
},
}
module.exports = {
meta: {
docs: {
description:
'enforce either `Buffer` or `require("buffer").Buffer`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/buffer.md",
},
type: "suggestion",
fixable: null,
schema: [{ enum: ["always", "never"] }],
messages: {
preferGlobal:
"Unexpected use of 'require(\"buffer\").Buffer'. Use the global variable 'Buffer' instead.",
preferModule:
"Unexpected use of the global variable 'Buffer'. Use 'require(\"buffer\").Buffer' instead.",
},
},
create(context) {
return {
"Program:exit"() {
checkForPreferGlobal(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,44 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const checkForPreferGlobal = require("../../util/check-prefer-global")
const trackMap = {
globals: {
console: { [READ]: true },
},
modules: {
console: { [READ]: true },
},
}
module.exports = {
meta: {
docs: {
description: 'enforce either `console` or `require("console")`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/console.md",
},
type: "suggestion",
fixable: null,
schema: [{ enum: ["always", "never"] }],
messages: {
preferGlobal:
"Unexpected use of 'require(\"console\")'. Use the global variable 'console' instead.",
preferModule:
"Unexpected use of the global variable 'console'. Use 'require(\"console\")' instead.",
},
},
create(context) {
return {
"Program:exit"() {
checkForPreferGlobal(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,44 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const checkForPreferGlobal = require("../../util/check-prefer-global")
const trackMap = {
globals: {
process: { [READ]: true },
},
modules: {
process: { [READ]: true },
},
}
module.exports = {
meta: {
docs: {
description: 'enforce either `process` or `require("process")`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/process.md",
},
type: "suggestion",
fixable: null,
schema: [{ enum: ["always", "never"] }],
messages: {
preferGlobal:
"Unexpected use of 'require(\"process\")'. Use the global variable 'process' instead.",
preferModule:
"Unexpected use of the global variable 'process'. Use 'require(\"process\")' instead.",
},
},
create(context) {
return {
"Program:exit"() {
checkForPreferGlobal(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,47 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const checkForPreferGlobal = require("../../util/check-prefer-global")
const trackMap = {
globals: {
TextDecoder: { [READ]: true },
},
modules: {
util: {
TextDecoder: { [READ]: true },
},
},
}
module.exports = {
meta: {
docs: {
description:
'enforce either `TextDecoder` or `require("util").TextDecoder`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/text-decoder.md",
},
type: "suggestion",
fixable: null,
schema: [{ enum: ["always", "never"] }],
messages: {
preferGlobal:
"Unexpected use of 'require(\"util\").TextDecoder'. Use the global variable 'TextDecoder' instead.",
preferModule:
"Unexpected use of the global variable 'TextDecoder'. Use 'require(\"util\").TextDecoder' instead.",
},
},
create(context) {
return {
"Program:exit"() {
checkForPreferGlobal(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,47 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const checkForPreferGlobal = require("../../util/check-prefer-global")
const trackMap = {
globals: {
TextEncoder: { [READ]: true },
},
modules: {
util: {
TextEncoder: { [READ]: true },
},
},
}
module.exports = {
meta: {
docs: {
description:
'enforce either `TextEncoder` or `require("util").TextEncoder`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/text-encoder.md",
},
type: "suggestion",
fixable: null,
schema: [{ enum: ["always", "never"] }],
messages: {
preferGlobal:
"Unexpected use of 'require(\"util\").TextEncoder'. Use the global variable 'TextEncoder' instead.",
preferModule:
"Unexpected use of the global variable 'TextEncoder'. Use 'require(\"util\").TextEncoder' instead.",
},
},
create(context) {
return {
"Program:exit"() {
checkForPreferGlobal(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,47 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const checkForPreferGlobal = require("../../util/check-prefer-global")
const trackMap = {
globals: {
URLSearchParams: { [READ]: true },
},
modules: {
url: {
URLSearchParams: { [READ]: true },
},
},
}
module.exports = {
meta: {
docs: {
description:
'enforce either `URLSearchParams` or `require("url").URLSearchParams`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/url-search-params.md",
},
type: "suggestion",
fixable: null,
schema: [{ enum: ["always", "never"] }],
messages: {
preferGlobal:
"Unexpected use of 'require(\"url\").URLSearchParams'. Use the global variable 'URLSearchParams' instead.",
preferModule:
"Unexpected use of the global variable 'URLSearchParams'. Use 'require(\"url\").URLSearchParams' instead.",
},
},
create(context) {
return {
"Program:exit"() {
checkForPreferGlobal(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,46 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { READ } = require("@eslint-community/eslint-utils")
const checkForPreferGlobal = require("../../util/check-prefer-global")
const trackMap = {
globals: {
URL: { [READ]: true },
},
modules: {
url: {
URL: { [READ]: true },
},
},
}
module.exports = {
meta: {
docs: {
description: 'enforce either `URL` or `require("url").URL`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-global/url.md",
},
type: "suggestion",
fixable: null,
schema: [{ enum: ["always", "never"] }],
messages: {
preferGlobal:
"Unexpected use of 'require(\"url\").URL'. Use the global variable 'URL' instead.",
preferModule:
"Unexpected use of the global variable 'URL'. Use 'require(\"url\").URL' instead.",
},
},
create(context) {
return {
"Program:exit"() {
checkForPreferGlobal(context, trackMap)
},
}
},
}

View file

@ -0,0 +1,78 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const {
CALL,
CONSTRUCT,
ReferenceTracker,
} = require("@eslint-community/eslint-utils")
const trackMap = {
dns: {
lookup: { [CALL]: true },
lookupService: { [CALL]: true },
Resolver: { [CONSTRUCT]: true },
getServers: { [CALL]: true },
resolve: { [CALL]: true },
resolve4: { [CALL]: true },
resolve6: { [CALL]: true },
resolveAny: { [CALL]: true },
resolveCname: { [CALL]: true },
resolveMx: { [CALL]: true },
resolveNaptr: { [CALL]: true },
resolveNs: { [CALL]: true },
resolvePtr: { [CALL]: true },
resolveSoa: { [CALL]: true },
resolveSrv: { [CALL]: true },
resolveTxt: { [CALL]: true },
reverse: { [CALL]: true },
setServers: { [CALL]: true },
},
}
trackMap["node:dns"] = trackMap.dns
module.exports = {
meta: {
docs: {
description: 'enforce `require("dns").promises`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-promises/dns.md",
},
fixable: null,
messages: {
preferPromises: "Use 'dns.promises.{{name}}()' instead.",
preferPromisesNew: "Use 'new dns.promises.{{name}}()' instead.",
},
schema: [],
type: "suggestion",
},
create(context) {
return {
"Program:exit"(node) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
const tracker = new ReferenceTracker(scope, { mode: "legacy" })
const references = [
...tracker.iterateCjsReferences(trackMap),
...tracker.iterateEsmReferences(trackMap),
]
for (const { node, path } of references) {
const name = path[path.length - 1]
const isClass = name[0] === name[0].toUpperCase()
context.report({
node,
messageId: isClass
? "preferPromisesNew"
: "preferPromises",
data: { name },
})
}
},
}
},
}

View file

@ -0,0 +1,76 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { CALL, ReferenceTracker } = require("@eslint-community/eslint-utils")
const trackMap = {
fs: {
access: { [CALL]: true },
copyFile: { [CALL]: true },
open: { [CALL]: true },
rename: { [CALL]: true },
truncate: { [CALL]: true },
rmdir: { [CALL]: true },
mkdir: { [CALL]: true },
readdir: { [CALL]: true },
readlink: { [CALL]: true },
symlink: { [CALL]: true },
lstat: { [CALL]: true },
stat: { [CALL]: true },
link: { [CALL]: true },
unlink: { [CALL]: true },
chmod: { [CALL]: true },
lchmod: { [CALL]: true },
lchown: { [CALL]: true },
chown: { [CALL]: true },
utimes: { [CALL]: true },
realpath: { [CALL]: true },
mkdtemp: { [CALL]: true },
writeFile: { [CALL]: true },
appendFile: { [CALL]: true },
readFile: { [CALL]: true },
},
}
trackMap["node:fs"] = trackMap.fs
module.exports = {
meta: {
docs: {
description: 'enforce `require("fs").promises`',
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-promises/fs.md",
},
fixable: null,
messages: {
preferPromises: "Use 'fs.promises.{{name}}()' instead.",
},
schema: [],
type: "suggestion",
},
create(context) {
return {
"Program:exit"(node) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
const tracker = new ReferenceTracker(scope, { mode: "legacy" })
const references = [
...tracker.iterateCjsReferences(trackMap),
...tracker.iterateEsmReferences(trackMap),
]
for (const { node, path } of references) {
const name = path[path.length - 1]
context.report({
node,
messageId: "preferPromises",
data: { name },
})
}
},
}
},
}

View file

@ -0,0 +1,163 @@
/* eslint-disable eslint-plugin/prefer-message-ids */
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const CodePathAnalyzer = safeRequire(
"eslint/lib/linter/code-path-analysis/code-path-analyzer",
"eslint/lib/code-path-analysis/code-path-analyzer"
)
const CodePathSegment = safeRequire(
"eslint/lib/linter/code-path-analysis/code-path-segment",
"eslint/lib/code-path-analysis/code-path-segment"
)
const CodePath = safeRequire(
"eslint/lib/linter/code-path-analysis/code-path",
"eslint/lib/code-path-analysis/code-path"
)
const originalLeaveNode =
CodePathAnalyzer && CodePathAnalyzer.prototype.leaveNode
/**
* Imports a specific module.
* @param {...string} moduleNames - module names to import.
* @returns {object|null} The imported object, or null.
*/
function safeRequire(...moduleNames) {
for (const moduleName of moduleNames) {
try {
return require(moduleName)
} catch (_err) {
// Ignore.
}
}
return null
}
/* istanbul ignore next */
/**
* Copied from https://github.com/eslint/eslint/blob/16fad5880bb70e9dddbeab8ed0f425ae51f5841f/lib/code-path-analysis/code-path-analyzer.js#L137
*
* @param {CodePathAnalyzer} analyzer - The instance.
* @param {ASTNode} node - The current AST node.
* @returns {void}
*/
function forwardCurrentToHead(analyzer, node) {
const codePath = analyzer.codePath
const state = CodePath.getState(codePath)
const currentSegments = state.currentSegments
const headSegments = state.headSegments
const end = Math.max(currentSegments.length, headSegments.length)
let i = 0
let currentSegment = null
let headSegment = null
// Fires leaving events.
for (i = 0; i < end; ++i) {
currentSegment = currentSegments[i]
headSegment = headSegments[i]
if (currentSegment !== headSegment && currentSegment) {
if (currentSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentEnd",
currentSegment,
node
)
}
}
}
// Update state.
state.currentSegments = headSegments
// Fires entering events.
for (i = 0; i < end; ++i) {
currentSegment = currentSegments[i]
headSegment = headSegments[i]
if (currentSegment !== headSegment && headSegment) {
CodePathSegment.markUsed(headSegment)
if (headSegment.reachable) {
analyzer.emitter.emit(
"onCodePathSegmentStart",
headSegment,
node
)
}
}
}
}
/**
* Checks whether a given node is `process.exit()` or not.
*
* @param {ASTNode} node - A node to check.
* @returns {boolean} `true` if the node is `process.exit()`.
*/
function isProcessExit(node) {
return (
node.type === "CallExpression" &&
node.callee.type === "MemberExpression" &&
node.callee.computed === false &&
node.callee.object.type === "Identifier" &&
node.callee.object.name === "process" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "exit"
)
}
/**
* The function to override `CodePathAnalyzer.prototype.leaveNode` in order to
* address `process.exit()` as throw.
*
* @this CodePathAnalyzer
* @param {ASTNode} node - A node to be left.
* @returns {void}
*/
function overrideLeaveNode(node) {
if (isProcessExit(node)) {
this.currentNode = node
forwardCurrentToHead(this, node)
CodePath.getState(this.codePath).makeThrow()
this.original.leaveNode(node)
this.currentNode = null
} else {
originalLeaveNode.call(this, node)
}
}
const visitor =
CodePathAnalyzer == null
? {}
: {
Program: function installProcessExitAsThrow() {
CodePathAnalyzer.prototype.leaveNode = overrideLeaveNode
},
"Program:exit": function restoreProcessExitAsThrow() {
CodePathAnalyzer.prototype.leaveNode = originalLeaveNode
},
}
module.exports = {
meta: {
docs: {
description:
"require that `process.exit()` expressions use the same code path as `throw`",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/process-exit-as-throw.md",
},
type: "problem",
fixable: null,
schema: [],
supported: CodePathAnalyzer != null,
},
create() {
return visitor
},
}

View file

@ -0,0 +1,173 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const getConvertPath = require("../util/get-convert-path")
const getPackageJson = require("../util/get-package-json")
const NODE_SHEBANG = "#!/usr/bin/env node\n"
const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u
const NODE_SHEBANG_PATTERN = /#!\/usr\/bin\/env node(?: [^\r\n]+?)?\n/u
function simulateNodeResolutionAlgorithm(filePath, binField) {
const possibilities = [filePath]
let newFilePath = filePath.replace(/\.js$/u, "")
possibilities.push(newFilePath)
newFilePath = newFilePath.replace(/[/\\]index$/u, "")
possibilities.push(newFilePath)
return possibilities.includes(binField)
}
/**
* Checks whether or not a given path is a `bin` file.
*
* @param {string} filePath - A file path to check.
* @param {string|object|undefined} binField - A value of the `bin` field of `package.json`.
* @param {string} basedir - A directory path that `package.json` exists.
* @returns {boolean} `true` if the file is a `bin` file.
*/
function isBinFile(filePath, binField, basedir) {
if (!binField) {
return false
}
if (typeof binField === "string") {
return simulateNodeResolutionAlgorithm(
filePath,
path.resolve(basedir, binField)
)
}
return Object.keys(binField).some(key =>
simulateNodeResolutionAlgorithm(
filePath,
path.resolve(basedir, binField[key])
)
)
}
/**
* Gets the shebang line (includes a line ending) from a given code.
*
* @param {SourceCode} sourceCode - A source code object to check.
* @returns {{length: number, bom: boolean, shebang: string, cr: boolean}}
* shebang's information.
* `retv.shebang` is an empty string if shebang doesn't exist.
*/
function getShebangInfo(sourceCode) {
const m = SHEBANG_PATTERN.exec(sourceCode.text)
return {
bom: sourceCode.hasBOM,
cr: Boolean(m && m[2]),
length: (m && m[0].length) || 0,
shebang: (m && m[1] && `${m[1]}\n`) || "",
}
}
module.exports = {
meta: {
docs: {
description: "require correct usage of shebang",
recommended: true,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/shebang.md",
},
type: "problem",
fixable: "code",
schema: [
{
type: "object",
properties: {
//
convertPath: getConvertPath.schema,
},
additionalProperties: false,
},
],
messages: {
unexpectedBOM: "This file must not have Unicode BOM.",
expectedLF: "This file must have Unix linebreaks (LF).",
expectedHashbangNode:
'This file needs shebang "#!/usr/bin/env node".',
expectedHashbang: "This file needs no shebang.",
},
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
let filePath = context.filename ?? context.getFilename()
if (filePath === "<input>") {
return {}
}
filePath = path.resolve(filePath)
const p = getPackageJson(filePath)
if (!p) {
return {}
}
const basedir = path.dirname(p.filePath)
filePath = path.join(
basedir,
getConvertPath(context)(
path.relative(basedir, filePath).replace(/\\/gu, "/")
)
)
const needsShebang = isBinFile(filePath, p.bin, basedir)
const info = getShebangInfo(sourceCode)
return {
Program(node) {
if (
needsShebang
? NODE_SHEBANG_PATTERN.test(info.shebang)
: !info.shebang
) {
// Good the shebang target.
// Checks BOM and \r.
if (needsShebang && info.bom) {
context.report({
node,
messageId: "unexpectedBOM",
fix(fixer) {
return fixer.removeRange([-1, 0])
},
})
}
if (needsShebang && info.cr) {
context.report({
node,
messageId: "expectedLF",
fix(fixer) {
const index = sourceCode.text.indexOf("\r")
return fixer.removeRange([index, index + 1])
},
})
}
} else if (needsShebang) {
// Shebang is lacking.
context.report({
node,
messageId: "expectedHashbangNode",
fix(fixer) {
return fixer.replaceTextRange(
[-1, info.length],
NODE_SHEBANG
)
},
})
} else {
// Shebang is extra.
context.report({
node,
messageId: "expectedHashbang",
fix(fixer) {
return fixer.removeRange([0, info.length])
},
})
}
},
}
},
}

View file

@ -0,0 +1,58 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const SKIP_TIME = 5000
/**
* The class of cache.
* The cache will dispose of each value if the value has not been accessed
* during 5 seconds.
*/
module.exports = class Cache {
/**
* Initialize this cache instance.
*/
constructor() {
this.map = new Map()
}
/**
* Get the cached value of the given key.
* @param {any} key The key to get.
* @returns {any} The cached value or null.
*/
get(key) {
const entry = this.map.get(key)
const now = Date.now()
if (entry) {
if (entry.expire > now) {
entry.expire = now + SKIP_TIME
return entry.value
}
this.map.delete(key)
}
return null
}
/**
* Set the value of the given key.
* @param {any} key The key to set.
* @param {any} value The value to set.
* @returns {void}
*/
set(key, value) {
const entry = this.map.get(key)
const expire = Date.now() + SKIP_TIME
if (entry) {
entry.value = value
entry.expire = expire
} else {
this.map.set(key, { value, expire })
}
}
}

View file

@ -0,0 +1,63 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const exists = require("./exists")
const getAllowModules = require("./get-allow-modules")
const isTypescript = require("./is-typescript")
const mapTypescriptExtension = require("../util/map-typescript-extension")
/**
* Checks whether or not each requirement target exists.
*
* It looks up the target according to the logic of Node.js.
* See Also: https://nodejs.org/api/modules.html
*
* @param {RuleContext} context - A context to report.
* @param {ImportTarget[]} targets - A list of target information to check.
* @returns {void}
*/
exports.checkExistence = function checkExistence(context, targets) {
const allowed = new Set(getAllowModules(context))
for (const target of targets) {
const missingModule =
target.moduleName != null &&
!allowed.has(target.moduleName) &&
target.filePath == null
let missingFile = target.moduleName == null && !exists(target.filePath)
if (missingFile && isTypescript(context)) {
const parsed = path.parse(target.filePath)
const reversedExts = mapTypescriptExtension(
context,
target.filePath,
parsed.ext,
true
)
const reversedPaths = reversedExts.map(
reversedExt =>
path.resolve(parsed.dir, parsed.name) + reversedExt
)
missingFile = reversedPaths.every(
reversedPath =>
target.moduleName == null && !exists(reversedPath)
)
}
if (missingModule || missingFile) {
context.report({
node: target.node,
loc: target.node.loc,
messageId: "notFound",
data: target,
})
}
}
}
exports.messages = {
notFound: '"{{name}}" is not found.',
}

View file

@ -0,0 +1,56 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const getAllowModules = require("./get-allow-modules")
const getPackageJson = require("./get-package-json")
/**
* Checks whether or not each requirement target is published via package.json.
*
* It reads package.json and checks the target exists in `dependencies`.
*
* @param {RuleContext} context - A context to report.
* @param {string} filePath - The current file path.
* @param {ImportTarget[]} targets - A list of target information to check.
* @returns {void}
*/
exports.checkExtraneous = function checkExtraneous(context, filePath, targets) {
const packageInfo = getPackageJson(filePath)
if (!packageInfo) {
return
}
const allowed = new Set(getAllowModules(context))
const dependencies = new Set(
[packageInfo.name].concat(
Object.keys(packageInfo.dependencies || {}),
Object.keys(packageInfo.devDependencies || {}),
Object.keys(packageInfo.peerDependencies || {}),
Object.keys(packageInfo.optionalDependencies || {})
)
)
for (const target of targets) {
const extraneous =
target.moduleName != null &&
target.filePath != null &&
!dependencies.has(target.moduleName) &&
!allowed.has(target.moduleName)
if (extraneous) {
context.report({
node: target.node,
loc: target.node.loc,
messageId: "extraneous",
data: target,
})
}
}
}
exports.messages = {
extraneous: '"{{moduleName}}" is extraneous.',
}

View file

@ -0,0 +1,72 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { ReferenceTracker } = require("@eslint-community/eslint-utils")
const extendTrackmapWithNodePrefix = require("./extend-trackmap-with-node-prefix")
/**
* Verifier for `prefer-global/*` rules.
*/
class Verifier {
/**
* Initialize this instance.
* @param {RuleContext} context The rule context to report.
* @param {{modules:object,globals:object}} trackMap The track map.
*/
constructor(context, trackMap) {
this.context = context
this.trackMap = trackMap
this.verify =
context.options[0] === "never"
? this.verifyToPreferModules
: this.verifyToPreferGlobals
}
/**
* Verify the code to suggest the use of globals.
* @returns {void}
*/
verifyToPreferGlobals() {
const { context, trackMap } = this
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const scope =
sourceCode.getScope?.(sourceCode.ast) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
const tracker = new ReferenceTracker(scope, {
mode: "legacy",
})
const modules = extendTrackmapWithNodePrefix(trackMap.modules)
for (const { node } of [
...tracker.iterateCjsReferences(modules),
...tracker.iterateEsmReferences(modules),
]) {
context.report({ node, messageId: "preferGlobal" })
}
}
/**
* Verify the code to suggest the use of modules.
* @returns {void}
*/
verifyToPreferModules() {
const { context, trackMap } = this
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const scope =
sourceCode.getScope?.(sourceCode.ast) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
const tracker = new ReferenceTracker(scope)
for (const { node } of tracker.iterateGlobalReferences(
trackMap.globals
)) {
context.report({ node, messageId: "preferModule" })
}
}
}
module.exports = function checkForPreferGlobal(context, trackMap) {
new Verifier(context, trackMap).verify()
}

View file

@ -0,0 +1,87 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const getAllowModules = require("./get-allow-modules")
const getConvertPath = require("./get-convert-path")
const getNpmignore = require("./get-npmignore")
const getPackageJson = require("./get-package-json")
/**
* Checks whether or not each requirement target is published via package.json.
*
* It reads package.json and checks the target exists in `dependencies`.
*
* @param {RuleContext} context - A context to report.
* @param {string} filePath - The current file path.
* @param {ImportTarget[]} targets - A list of target information to check.
* @returns {void}
*/
exports.checkPublish = function checkPublish(context, filePath, targets) {
const packageInfo = getPackageJson(filePath)
if (!packageInfo) {
return
}
// Private packages are never published so we don't need to check the imported dependencies either.
// More information: https://docs.npmjs.com/cli/v8/configuring-npm/package-json#private
if (packageInfo.private === true) {
return
}
const allowed = new Set(getAllowModules(context))
const convertPath = getConvertPath(context)
const basedir = path.dirname(packageInfo.filePath)
const toRelative = fullPath => {
const retv = path.relative(basedir, fullPath).replace(/\\/gu, "/")
return convertPath(retv)
}
const npmignore = getNpmignore(filePath)
const devDependencies = new Set(
Object.keys(packageInfo.devDependencies || {})
)
const dependencies = new Set(
[].concat(
Object.keys(packageInfo.dependencies || {}),
Object.keys(packageInfo.peerDependencies || {}),
Object.keys(packageInfo.optionalDependencies || {})
)
)
if (!npmignore.match(toRelative(filePath))) {
// This file is published, so this cannot import private files.
for (const target of targets) {
const isPrivateFile = () => {
if (target.moduleName != null) {
return false
}
const relativeTargetPath = toRelative(target.filePath)
return (
relativeTargetPath !== "" &&
npmignore.match(relativeTargetPath)
)
}
const isDevPackage = () =>
target.moduleName != null &&
devDependencies.has(target.moduleName) &&
!dependencies.has(target.moduleName) &&
!allowed.has(target.moduleName)
if (isPrivateFile() || isDevPackage()) {
context.report({
node: target.node,
loc: target.node.loc,
messageId: "notPublished",
data: { name: target.moduleName || target.name },
})
}
}
}
}
exports.messages = {
notPublished: '"{{name}}" is not published.',
}

View file

@ -0,0 +1,114 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const { Minimatch } = require("minimatch")
/** @typedef {import("../util/import-target")} ImportTarget */
/**
* @typedef {Object} DefinitionData
* @property {string | string[]} name The name to disallow.
* @property {string} [message] The custom message to show.
*/
/**
* Check if matched or not.
* @param {InstanceType<Minimatch>} matcher The matcher.
* @param {boolean} absolute The flag that the matcher is for absolute paths.
* @param {ImportTarget} importee The importee information.
*/
function match(matcher, absolute, { filePath, name }) {
if (absolute) {
return filePath != null && matcher.match(filePath)
}
return matcher.match(name)
}
/** Restriction. */
class Restriction {
/**
* Initialize this restriction.
* @param {DefinitionData} def The definition of a restriction.
*/
constructor({ name, message }) {
const names = Array.isArray(name) ? name : [name]
const matchers = names.map(raw => {
const negate = raw[0] === "!" && raw[1] !== "("
const pattern = negate ? raw.slice(1) : raw
const absolute = path.isAbsolute(pattern)
const matcher = new Minimatch(pattern, { dot: true })
return { absolute, matcher, negate }
})
this.matchers = matchers
this.message = message ? ` ${message}` : ""
}
/**
* Check if a given importee is disallowed.
* @param {ImportTarget} importee The importee to check.
* @returns {boolean} `true` if the importee is disallowed.
*/
match(importee) {
return this.matchers.reduce(
(ret, { absolute, matcher, negate }) =>
negate
? ret && !match(matcher, absolute, importee)
: ret || match(matcher, absolute, importee),
false
)
}
}
/**
* Create a restriction.
* @param {string | DefinitionData} def A definition.
* @returns {Restriction} Created restriction.
*/
function createRestriction(def) {
if (typeof def === "string") {
return new Restriction({ name: def })
}
return new Restriction(def)
}
/**
* Create restrictions.
* @param {(string | DefinitionData | GlobDefinition)[]} defs Definitions.
* @returns {(Restriction | GlobRestriction)[]} Created restrictions.
*/
function createRestrictions(defs) {
return (defs || []).map(createRestriction)
}
/**
* Checks if given importees are disallowed or not.
* @param {RuleContext} context - A context to report.
* @param {ImportTarget[]} targets - A list of target information to check.
* @returns {void}
*/
exports.checkForRestriction = function checkForRestriction(context, targets) {
const restrictions = createRestrictions(context.options[0])
for (const target of targets) {
const restriction = restrictions.find(r => r.match(target))
if (restriction) {
context.report({
node: target.node,
messageId: "restricted",
data: {
name: target.name,
customMessage: restriction.message,
},
})
}
}
}
exports.messages = {
restricted:
"'{{name}}' module is restricted from being used.{{customMessage}}",
}

View file

@ -0,0 +1,118 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const { Range, lt, major } = require("semver") // eslint-disable-line no-unused-vars
const { ReferenceTracker } = require("@eslint-community/eslint-utils")
const getConfiguredNodeVersion = require("./get-configured-node-version")
const getSemverRange = require("./get-semver-range")
const unprefixNodeColon = require("./unprefix-node-colon")
/**
* @typedef {Object} SupportInfo
* @property {string | null} supported The stably supported version. If `null` is present, it hasn't been supported yet.
* @property {string[]} [backported] The backported versions.
* @property {string} [experimental] The added version as experimental.
*/
/**
* Parses the options.
* @param {RuleContext} context The rule context.
* @returns {{version:Range,ignores:Set<string>}} Parsed value.
*/
function parseOptions(context) {
const raw = context.options[0] || {}
const version = getConfiguredNodeVersion(context)
const ignores = new Set(raw.ignores || [])
return Object.freeze({ version, ignores })
}
/**
* Check if it has been supported.
* @param {SupportInfo} info The support info.
* @param {Range} configured The configured version range.
*/
function isSupported({ backported, supported }, configured) {
if (
backported &&
backported.length >= 2 &&
!backported.every((v, i) => i === 0 || lt(backported[i - 1], v))
) {
throw new Error("Invalid BackportConfiguration")
}
if (supported == null) {
return false
}
if (backported == null || backported.length === 0) {
return !configured.intersects(getSemverRange(`<${supported}`))
}
return !configured.intersects(
getSemverRange(
[...backported, supported]
.map((v, i) => (i === 0 ? `<${v}` : `>=${major(v)}.0.0 <${v}`))
.join(" || ")
)
)
}
/**
* Get the formatted text of a given supported version.
* @param {SupportInfo} info The support info.
*/
function supportedVersionToString({ backported, supported }) {
if (supported == null) {
return "(none yet)"
}
if (backported == null || backported.length === 0) {
return supported
}
return `${supported} (backported: ^${backported.join(", ^")})`
}
/**
* Verify the code to report unsupported APIs.
* @param {RuleContext} context The rule context.
* @param {{modules:object,globals:object}} trackMap The map for APIs to report.
* @returns {void}
*/
module.exports.checkUnsupportedBuiltins = function checkUnsupportedBuiltins(
context,
trackMap
) {
const options = parseOptions(context)
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const scope = sourceCode.getScope?.(sourceCode.ast) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
const tracker = new ReferenceTracker(scope, { mode: "legacy" })
const references = [
...tracker.iterateCjsReferences(trackMap.modules || {}),
...tracker.iterateEsmReferences(trackMap.modules || {}),
...tracker.iterateGlobalReferences(trackMap.globals || {}),
]
for (const { node, path, info } of references) {
const name = unprefixNodeColon(path.join("."))
const supported = isSupported(info, options.version)
if (!supported && !options.ignores.has(name)) {
context.report({
node,
messageId: "unsupported",
data: {
name,
supported: supportedVersionToString(info),
version: options.version.raw,
},
})
}
}
}
exports.messages = {
unsupported:
"The '{{name}}' is not supported until Node.js {{supported}}. The configured version range is '{{version}}'.",
}

View file

@ -0,0 +1,40 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* See LICENSE file in root directory for full license.
*/
"use strict"
const { CALL, CONSTRUCT, READ } = require("@eslint-community/eslint-utils")
const unprefixNodeColon = require("./unprefix-node-colon")
/**
* Enumerate property names of a given object recursively.
* @param {object} trackMap The map for APIs to enumerate.
* @param {string[]|undefined} path The path to the current map.
* @returns {IterableIterator<string>} The property names of the map.
*/
function* enumeratePropertyNames(trackMap, path = []) {
for (const key of Object.keys(trackMap)) {
const value = trackMap[key]
if (typeof value !== "object") {
continue
}
path.push(key)
const name = unprefixNodeColon(path.join("."))
if (value[CALL]) {
yield `${name}()`
}
if (value[CONSTRUCT]) {
yield `new ${name}()`
}
if (value[READ]) {
yield name
}
yield* enumeratePropertyNames(value, path)
path.pop()
}
}
module.exports = enumeratePropertyNames

View file

@ -0,0 +1,58 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const fs = require("fs")
const path = require("path")
const Cache = require("./cache")
const ROOT = /^(?:[/.]|\.\.|[A-Z]:\\|\\\\)(?:[/\\]\.\.)*$/u
const cache = new Cache()
/**
* Check whether the file exists or not.
* @param {string} filePath The file path to check.
* @returns {boolean} `true` if the file exists.
*/
function existsCaseSensitive(filePath) {
let dirPath = filePath
while (dirPath !== "" && !ROOT.test(dirPath)) {
const fileName = path.basename(dirPath)
dirPath = path.dirname(dirPath)
if (fs.readdirSync(dirPath).indexOf(fileName) === -1) {
return false
}
}
return true
}
/**
* Checks whether or not the file of a given path exists.
*
* @param {string} filePath - A file path to check.
* @returns {boolean} `true` if the file of a given path exists.
*/
module.exports = function exists(filePath) {
let result = cache.get(filePath)
if (result == null) {
try {
const relativePath = path.relative(process.cwd(), filePath)
result =
fs.statSync(relativePath).isFile() &&
existsCaseSensitive(relativePath)
} catch (error) {
if (error.code !== "ENOENT") {
throw error
}
result = false
}
cache.set(filePath, result)
}
return result
}

View file

@ -0,0 +1,21 @@
"use strict"
const isCoreModule = require("is-core-module")
/**
* Extend trackMap.modules with `node:` prefixed modules
* @param {Object} modules Like `{assert: foo}`
* @returns {Object} Like `{assert: foo}, "node:assert": foo}`
*/
module.exports = function extendTrackMapWithNodePrefix(modules) {
const ret = {
...modules,
...Object.fromEntries(
Object.entries(modules)
.map(([name, value]) => [`node:${name}`, value])
// Note: "999" arbitrary to check current/future Node.js version
.filter(([name]) => isCoreModule(name, "999"))
),
}
return ret
}

View file

@ -0,0 +1,49 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const DEFAULT_VALUE = Object.freeze([])
/**
* Gets `allowModules` property from a given option object.
*
* @param {object|undefined} option - An option object to get.
* @returns {string[]|null} The `allowModules` value, or `null`.
*/
function get(option) {
if (option && option.allowModules && Array.isArray(option.allowModules)) {
return option.allowModules.map(String)
}
return null
}
/**
* Gets "allowModules" setting.
*
* 1. This checks `options` property, then returns it if exists.
* 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
* 3. This returns `[]`.
*
* @param {RuleContext} context - The rule context.
* @returns {string[]} A list of extensions.
*/
module.exports = function getAllowModules(context) {
return (
get(context.options && context.options[0]) ||
get(
context.settings && (context.settings.n || context.settings.node)
) ||
DEFAULT_VALUE
)
}
module.exports.schema = {
type: "array",
items: {
type: "string",
pattern: "^(?:@[a-zA-Z0-9_\\-.]+/)?[a-zA-Z0-9_\\-.]+$",
},
uniqueItems: true,
}

View file

@ -0,0 +1,61 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* See LICENSE file in root directory for full license.
*/
"use strict"
const { Range } = require("semver") // eslint-disable-line no-unused-vars
const getPackageJson = require("./get-package-json")
const getSemverRange = require("./get-semver-range")
/**
* Gets `version` property from a given option object.
*
* @param {object|undefined} option - An option object to get.
* @returns {string[]|null} The `allowModules` value, or `null`.
*/
function get(option) {
if (option && option.version) {
return option.version
}
return null
}
/**
* Get the `engines.node` field of package.json.
* @param {string} filename The path to the current linting file.
* @returns {Range|null} The range object of the `engines.node` field.
*/
function getEnginesNode(filename) {
const info = getPackageJson(filename)
return getSemverRange(info && info.engines && info.engines.node)
}
/**
* Gets version configuration.
*
* 1. Parse a given version then return it if it's valid.
* 2. Look package.json up and parse `engines.node` then return it if it's valid.
* 3. Return `>=16.0.0`.
*
* @param {string|undefined} version The version range text.
* @param {string} filename The path to the current linting file.
* This will be used to look package.json up if `version` is not a valid version range.
* @returns {Range} The configured version range.
*/
module.exports = function getConfiguredNodeVersion(context) {
const version =
get(context.options && context.options[0]) ||
get(context.settings && (context.settings.n || context.settings.node))
const filePath = context.filename ?? context.getFilename()
return (
getSemverRange(version) ||
getEnginesNode(filePath) ||
getSemverRange(">=16.0.0")
)
}
module.exports.schema = {
type: "string",
}

View file

@ -0,0 +1,191 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const Minimatch = require("minimatch").Minimatch
/**
* @param {any} x - An any value.
* @returns {any} Always `x`.
*/
function identity(x) {
return x
}
/**
* Converts old-style value to new-style value.
*
* @param {any} x - The value to convert.
* @returns {({include: string[], exclude: string[], replace: string[]})[]} Normalized value.
*/
function normalizeValue(x) {
if (Array.isArray(x)) {
return x
}
return Object.keys(x).map(pattern => ({
include: [pattern],
exclude: [],
replace: x[pattern],
}))
}
/**
* Ensures the given value is a string array.
*
* @param {any} x - The value to ensure.
* @returns {string[]} The string array.
*/
function toStringArray(x) {
if (Array.isArray(x)) {
return x.map(String)
}
return []
}
/**
* Creates the function which checks whether a file path is matched with the given pattern or not.
*
* @param {string[]} includePatterns - The glob patterns to include files.
* @param {string[]} excludePatterns - The glob patterns to exclude files.
* @returns {function} Created predicate function.
*/
function createMatch(includePatterns, excludePatterns) {
const include = includePatterns.map(pattern => new Minimatch(pattern))
const exclude = excludePatterns.map(pattern => new Minimatch(pattern))
return filePath =>
include.some(m => m.match(filePath)) &&
!exclude.some(m => m.match(filePath))
}
/**
* Creates a function which replaces a given path.
*
* @param {RegExp} fromRegexp - A `RegExp` object to replace.
* @param {string} toStr - A new string to replace.
* @returns {function} A function which replaces a given path.
*/
function defineConvert(fromRegexp, toStr) {
return filePath => filePath.replace(fromRegexp, toStr)
}
/**
* Combines given converters.
* The result function converts a given path with the first matched converter.
*
* @param {{match: function, convert: function}} converters - A list of converters to combine.
* @returns {function} A function which replaces a given path.
*/
function combine(converters) {
return filePath => {
for (const converter of converters) {
if (converter.match(filePath)) {
return converter.convert(filePath)
}
}
return filePath
}
}
/**
* Parses `convertPath` property from a given option object.
*
* @param {object|undefined} option - An option object to get.
* @returns {function|null} A function which converts a path., or `null`.
*/
function parse(option) {
if (
!option ||
!option.convertPath ||
typeof option.convertPath !== "object"
) {
return null
}
const converters = []
for (const pattern of normalizeValue(option.convertPath)) {
const include = toStringArray(pattern.include)
const exclude = toStringArray(pattern.exclude)
const fromRegexp = new RegExp(String(pattern.replace[0]))
const toStr = String(pattern.replace[1])
converters.push({
match: createMatch(include, exclude),
convert: defineConvert(fromRegexp, toStr),
})
}
return combine(converters)
}
/**
* Gets "convertPath" setting.
*
* 1. This checks `options` property, then returns it if exists.
* 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
* 3. This returns a function of identity.
*
* @param {RuleContext} context - The rule context.
* @returns {function} A function which converts a path.
*/
module.exports = function getConvertPath(context) {
return (
parse(context.options && context.options[0]) ||
parse(
context.settings && (context.settings.n || context.settings.node)
) ||
identity
)
}
/**
* JSON Schema for `convertPath` option.
*/
module.exports.schema = {
anyOf: [
{
type: "object",
properties: {},
patternProperties: {
"^.+$": {
type: "array",
items: { type: "string" },
minItems: 2,
maxItems: 2,
},
},
additionalProperties: false,
},
{
type: "array",
items: {
type: "object",
properties: {
include: {
type: "array",
items: { type: "string" },
minItems: 1,
uniqueItems: true,
},
exclude: {
type: "array",
items: { type: "string" },
uniqueItems: true,
},
replace: {
type: "array",
items: { type: "string" },
minItems: 2,
maxItems: 2,
},
},
additionalProperties: false,
required: ["include", "replace"],
},
minItems: 1,
},
],
}

View file

@ -0,0 +1,187 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const fs = require("fs")
const path = require("path")
const ignore = require("ignore")
const Cache = require("./cache")
const exists = require("./exists")
const getPackageJson = require("./get-package-json")
const cache = new Cache()
const PARENT_RELATIVE_PATH = /^\.\./u
const NEVER_IGNORED =
/^(?:readme\.[^.]*|(?:licen[cs]e|changes|changelog|history)(?:\.[^.]*)?)$/iu
/**
* Checks whether or not a given file name is a relative path to a ancestor
* directory.
*
* @param {string} filePath - A file name to check.
* @returns {boolean} `true` if the file name is a relative path to a ancestor
* directory.
*/
function isAncestorFiles(filePath) {
return PARENT_RELATIVE_PATH.test(filePath)
}
/**
* @param {function} f - A function.
* @param {function} g - A function.
* @returns {function} A logical-and function of `f` and `g`.
*/
function and(f, g) {
return filePath => f(filePath) && g(filePath)
}
/**
* @param {function} f - A function.
* @param {function} g - A function.
* @param {function|null} h - A function.
* @returns {function} A logical-or function of `f`, `g`, and `h`.
*/
function or(f, g, h) {
return filePath => f(filePath) || g(filePath) || (h && h(filePath))
}
/**
* @param {function} f - A function.
* @returns {function} A logical-not function of `f`.
*/
function not(f) {
return filePath => !f(filePath)
}
/**
* Creates a function which checks whether or not a given file is ignoreable.
*
* @param {object} p - An object of package.json.
* @returns {function} A function which checks whether or not a given file is ignoreable.
*/
function filterNeverIgnoredFiles(p) {
const basedir = path.dirname(p.filePath)
const mainFilePath =
typeof p.main === "string" ? path.join(basedir, p.main) : null
return filePath =>
path.join(basedir, filePath) !== mainFilePath &&
filePath !== "package.json" &&
!NEVER_IGNORED.test(path.relative(basedir, filePath))
}
/**
* Creates a function which checks whether or not a given file should be ignored.
*
* @param {string[]|null} files - File names of whitelist.
* @returns {function|null} A function which checks whether or not a given file should be ignored.
*/
function parseWhiteList(files) {
if (!files || !Array.isArray(files)) {
return null
}
const ig = ignore()
const igN = ignore()
let hasN = false
for (const file of files) {
if (typeof file === "string" && file) {
const body = path.posix
.normalize(file.replace(/^!/u, ""))
.replace(/\/+$/u, "")
if (file.startsWith("!")) {
igN.add(`${body}`)
igN.add(`${body}/**`)
hasN = true
} else {
ig.add(`/${body}`)
ig.add(`/${body}/**`)
}
}
}
return hasN
? or(ig.createFilter(), not(igN.createFilter()))
: ig.createFilter()
}
/**
* Creates a function which checks whether or not a given file should be ignored.
*
* @param {string} basedir - The directory path "package.json" exists.
* @param {boolean} filesFieldExists - `true` if `files` field of `package.json` exists.
* @returns {function|null} A function which checks whether or not a given file should be ignored.
*/
function parseNpmignore(basedir, filesFieldExists) {
let filePath = path.join(basedir, ".npmignore")
if (!exists(filePath)) {
if (filesFieldExists) {
return null
}
filePath = path.join(basedir, ".gitignore")
if (!exists(filePath)) {
return null
}
}
const ig = ignore()
ig.add(fs.readFileSync(filePath, "utf8"))
return not(ig.createFilter())
}
/**
* Gets an object to check whether a given path should be ignored or not.
* The object is created from:
*
* - `files` field of `package.json`
* - `.npmignore`
*
* @param {string} startPath - A file path to lookup.
* @returns {object}
* An object to check whther or not a given path should be ignored.
* The object has a method `match`.
* `match` returns `true` if a given file path should be ignored.
*/
module.exports = function getNpmignore(startPath) {
const retv = { match: isAncestorFiles }
const p = getPackageJson(startPath)
if (p) {
const data = cache.get(p.filePath)
if (data) {
return data
}
const filesIgnore = parseWhiteList(p.files)
const npmignoreIgnore = parseNpmignore(
path.dirname(p.filePath),
Boolean(filesIgnore)
)
if (filesIgnore && npmignoreIgnore) {
retv.match = and(
filterNeverIgnoredFiles(p),
or(isAncestorFiles, filesIgnore, npmignoreIgnore)
)
} else if (filesIgnore) {
retv.match = and(
filterNeverIgnoredFiles(p),
or(isAncestorFiles, filesIgnore)
)
} else if (npmignoreIgnore) {
retv.match = and(
filterNeverIgnoredFiles(p),
or(isAncestorFiles, npmignoreIgnore)
)
}
cache.set(p.filePath, retv)
}
return retv
}

View file

@ -0,0 +1,75 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const fs = require("fs")
const path = require("path")
const Cache = require("./cache")
const cache = new Cache()
/**
* Reads the `package.json` data in a given path.
*
* Don't cache the data.
*
* @param {string} dir - The path to a directory to read.
* @returns {object|null} The read `package.json` data, or null.
*/
function readPackageJson(dir) {
const filePath = path.join(dir, "package.json")
try {
const text = fs.readFileSync(filePath, "utf8")
const data = JSON.parse(text)
if (typeof data === "object" && data !== null) {
data.filePath = filePath
return data
}
} catch (_err) {
// do nothing.
}
return null
}
/**
* Gets a `package.json` data.
* The data is cached if found, then it's used after.
*
* @param {string} [startPath] - A file path to lookup.
* @returns {object|null} A found `package.json` data or `null`.
* This object have additional property `filePath`.
*/
module.exports = function getPackageJson(startPath = "a.js") {
const startDir = path.dirname(path.resolve(startPath))
let dir = startDir
let prevDir = ""
let data = null
do {
data = cache.get(dir)
if (data) {
if (dir !== startDir) {
cache.set(startDir, data)
}
return data
}
data = readPackageJson(dir)
if (data) {
cache.set(dir, data)
cache.set(startDir, data)
return data
}
// Go to next.
prevDir = dir
dir = path.resolve(dir, "..")
} while (dir !== prevDir)
cache.set(startDir, null)
return null
}

View file

@ -0,0 +1,46 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const DEFAULT_VALUE = Object.freeze([])
/**
* Gets `resolvePaths` property from a given option object.
*
* @param {object|undefined} option - An option object to get.
* @returns {string[]|null} The `allowModules` value, or `null`.
*/
function get(option) {
if (option && option.resolvePaths && Array.isArray(option.resolvePaths)) {
return option.resolvePaths.map(String)
}
return null
}
/**
* Gets "resolvePaths" setting.
*
* 1. This checks `options` property, then returns it if exists.
* 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
* 3. This returns `[]`.
*
* @param {RuleContext} context - The rule context.
* @returns {string[]} A list of extensions.
*/
module.exports = function getResolvePaths(context, optionIndex = 0) {
return (
get(context.options && context.options[optionIndex]) ||
get(
context.settings && (context.settings.n || context.settings.node)
) ||
DEFAULT_VALUE
)
}
module.exports.schema = {
type: "array",
items: { type: "string" },
uniqueItems: true,
}

View file

@ -0,0 +1,30 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* See LICENSE file in root directory for full license.
*/
"use strict"
const { Range } = require("semver")
const cache = new Map()
/**
* Get the `semver.Range` object of a given range text.
* @param {string} x The text expression for a semver range.
* @returns {Range|null} The range object of a given range text.
* It's null if the `x` is not a valid range text.
*/
module.exports = function getSemverRange(x) {
const s = String(x)
let ret = cache.get(s) || null
if (!ret) {
try {
ret = new Range(s)
} catch (_error) {
// Ignore parsing error.
}
cache.set(s, ret)
}
return ret
}

View file

@ -0,0 +1,49 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const DEFAULT_VALUE = Object.freeze([".js", ".json", ".node"])
/**
* Gets `tryExtensions` property from a given option object.
*
* @param {object|undefined} option - An option object to get.
* @returns {string[]|null} The `tryExtensions` value, or `null`.
*/
function get(option) {
if (option && option.tryExtensions && Array.isArray(option.tryExtensions)) {
return option.tryExtensions.map(String)
}
return null
}
/**
* Gets "tryExtensions" setting.
*
* 1. This checks `options` property, then returns it if exists.
* 2. This checks `settings.n` | `settings.node` property, then returns it if exists.
* 3. This returns `[".js", ".json", ".node"]`.
*
* @param {RuleContext} context - The rule context.
* @returns {string[]} A list of extensions.
*/
module.exports = function getTryExtensions(context, optionIndex = 0) {
return (
get(context.options && context.options[optionIndex]) ||
get(
context.settings && (context.settings.n || context.settings.node)
) ||
DEFAULT_VALUE
)
}
module.exports.schema = {
type: "array",
items: {
type: "string",
pattern: "^\\.",
},
uniqueItems: true,
}

View file

@ -0,0 +1,31 @@
"use strict"
const { getTsconfig, parseTsconfig } = require("get-tsconfig")
const fsCache = new Map()
/**
* Attempts to get the ExtensionMap from the tsconfig given the path to the tsconfig file.
*
* @param {string} filename - The path to the tsconfig.json file
* @returns {import("get-tsconfig").TsConfigJsonResolved}
*/
function getTSConfig(filename) {
return parseTsconfig(filename, fsCache)
}
/**
* Attempts to get the ExtensionMap from the tsconfig of a given file.
*
* @param {string} filename - The path to the file we need to find the tsconfig.json of
* @returns {import("get-tsconfig").TsConfigResult}
*/
function getTSConfigForFile(filename) {
return getTsconfig(filename, "tsconfig.json", fsCache)
}
module.exports = {
getTSConfig,
getTSConfigForFile,
}
module.exports.schema = { type: "string" }

View file

@ -0,0 +1,147 @@
"use strict"
const { getTSConfig, getTSConfigForFile } = require("./get-tsconfig")
const DEFAULT_MAPPING = normalise([
["", ".js"],
[".ts", ".js"],
[".cts", ".cjs"],
[".mts", ".mjs"],
[".tsx", ".js"],
])
const PRESERVE_MAPPING = normalise([
["", ".js"],
[".ts", ".js"],
[".cts", ".cjs"],
[".mts", ".mjs"],
[".tsx", ".jsx"],
])
const tsConfigMapping = {
react: DEFAULT_MAPPING, // Emit .js files with JSX changed to the equivalent React.createElement calls
"react-jsx": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls
"react-jsxdev": DEFAULT_MAPPING, // Emit .js files with the JSX changed to _jsx calls
"react-native": DEFAULT_MAPPING, // Emit .js files with the JSX unchanged
preserve: PRESERVE_MAPPING, // Emit .jsx files with the JSX unchanged
}
/**
* @typedef {Object} ExtensionMap
* @property {Record<string, string>} forward Convert from typescript to javascript
* @property {Record<string, string[]>} backward Convert from javascript to typescript
*/
function normalise(typescriptExtensionMap) {
const forward = {}
const backward = {}
for (const [typescript, javascript] of typescriptExtensionMap) {
forward[typescript] = javascript
if (!typescript) {
continue
}
backward[javascript] ??= []
backward[javascript].push(typescript)
}
return { forward, backward }
}
/**
* Attempts to get the ExtensionMap from the resolved tsconfig.
*
* @param {import("get-tsconfig").TsConfigJsonResolved} [tsconfig] - The resolved tsconfig
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
*/
function getMappingFromTSConfig(tsconfig) {
const jsx = tsconfig?.compilerOptions?.jsx
if ({}.hasOwnProperty.call(tsConfigMapping, jsx)) {
return tsConfigMapping[jsx]
}
return null
}
/**
* Gets `typescriptExtensionMap` property from a given option object.
*
* @param {object|undefined} option - An option object to get.
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
*/
function get(option) {
if (
{}.hasOwnProperty.call(tsConfigMapping, option?.typescriptExtensionMap)
) {
return tsConfigMapping[option.typescriptExtensionMap]
}
if (Array.isArray(option?.typescriptExtensionMap)) {
return normalise(option.typescriptExtensionMap)
}
if (option?.tsconfigPath) {
return getMappingFromTSConfig(getTSConfig(option?.tsconfigPath))
}
return null
}
/**
* Attempts to get the ExtensionMap from the tsconfig of a given file.
*
* @param {string} filename - The filename we're getting from
* @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`.
*/
function getFromTSConfigFromFile(filename) {
return getMappingFromTSConfig(getTSConfigForFile(filename)?.config)
}
/**
* Gets "typescriptExtensionMap" setting.
*
* 1. This checks `options.typescriptExtensionMap`, if its an array then it gets returned.
* 2. This checks `options.typescriptExtensionMap`, if its a string, convert to the correct mapping.
* 3. This checks `settings.n.typescriptExtensionMap`, if its an array then it gets returned.
* 4. This checks `settings.node.typescriptExtensionMap`, if its an array then it gets returned.
* 5. This checks `settings.n.typescriptExtensionMap`, if its a string, convert to the correct mapping.
* 6. This checks `settings.node.typescriptExtensionMap`, if its a string, convert to the correct mapping.
* 7. This checks for a `tsconfig.json` `config.compilerOptions.jsx` property, if its a string, convert to the correct mapping.
* 8. This returns `PRESERVE_MAPPING`.
*
* @param {import("eslint").Rule.RuleContext} context - The rule context.
* @returns {string[]} A list of extensions.
*/
module.exports = function getTypescriptExtensionMap(context) {
const filename =
context.physicalFilename ??
context.getPhysicalFilename?.() ??
context.filename ??
context.getFilename?.() // TODO: remove context.get(PhysicalFilename|Filename) when dropping eslint < v10
return (
get(context.options?.[0]) ||
get(context.settings?.n ?? context.settings?.node) ||
getFromTSConfigFromFile(filename) ||
PRESERVE_MAPPING
)
}
module.exports.schema = {
oneOf: [
{
type: "array",
items: {
type: "array",
prefixItems: [
{ type: "string", pattern: "^(?:|\\.\\w+)$" },
{ type: "string", pattern: "^\\.\\w+$" },
],
additionalItems: false,
},
uniqueItems: true,
},
{
type: "string",
enum: Object.keys(tsConfigMapping),
},
],
}

View file

@ -0,0 +1,159 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const { pathToFileURL, fileURLToPath } = require("url")
const isBuiltin = require("is-builtin-module")
const resolve = require("resolve")
const {
defaultResolve: importResolve,
} = require("../converted-esm/import-meta-resolve")
/**
* Resolve the given id to file paths.
* @param {boolean} isModule The flag which indicates this id is a module.
* @param {string} id The id to resolve.
* @param {object} options The options of node-resolve module.
* It requires `options.basedir`.
* @param {'import' | 'require'} moduleType - whether the target was require-ed or imported
* @returns {string|null} The resolved path.
*/
function getFilePath(isModule, id, options, moduleType) {
if (moduleType === "import") {
const paths =
options.paths && options.paths.length > 0
? options.paths.map(p => path.resolve(process.cwd(), p))
: [options.basedir]
for (const aPath of paths) {
try {
const { url } = importResolve(id, {
parentURL: pathToFileURL(path.join(aPath, "dummy-file.mjs"))
.href,
conditions: ["node", "import", "require"],
})
if (url) {
return fileURLToPath(url)
}
} catch (e) {
continue
}
}
if (isModule) {
return null
}
return path.resolve(
(options.paths && options.paths[0]) || options.basedir,
id
)
} else {
try {
return resolve.sync(id, options)
} catch (_err) {
try {
const { url } = importResolve(id, {
parentURL: pathToFileURL(
path.join(options.basedir, "dummy-file.js")
).href,
conditions: ["node", "require"],
})
return fileURLToPath(url)
} catch (err) {
if (isModule) {
return null
}
return path.resolve(options.basedir, id)
}
}
}
}
function isNodeModule(name, options) {
try {
return require.resolve(name, options).startsWith(path.sep)
} catch {
return false
}
}
/**
* Gets the module name of a given path.
*
* e.g. `eslint/lib/ast-utils` -> `eslint`
*
* @param {string} nameOrPath - A path to get.
* @returns {string} The module name of the path.
*/
function getModuleName(nameOrPath) {
let end = nameOrPath.indexOf("/")
if (end !== -1 && nameOrPath[0] === "@") {
end = nameOrPath.indexOf("/", 1 + end)
}
return end === -1 ? nameOrPath : nameOrPath.slice(0, end)
}
/**
* Information of an import target.
*/
module.exports = class ImportTarget {
/**
* Initialize this instance.
* @param {ASTNode} node - The node of a `require()` or a module declaraiton.
* @param {string} name - The name of an import target.
* @param {object} options - The options of `node-resolve` module.
* @param {'import' | 'require'} moduleType - whether the target was require-ed or imported
*/
constructor(node, name, options, moduleType) {
const isModule = !/^(?:[./\\]|\w+:)/u.test(name)
/**
* The node of a `require()` or a module declaraiton.
* @type {ASTNode}
*/
this.node = node
/**
* The name of this import target.
* @type {string}
*/
this.name = name
/**
* What type of module is this
* @type {'unknown'|'relative'|'absolute'|'node'|'npm'|'http'|void}
*/
this.moduleType = "unknown"
if (name.startsWith("./") || name.startsWith(".\\")) {
this.moduleType = "relative"
} else if (name.startsWith("/") || name.startsWith("\\")) {
this.moduleType = "absolute"
} else if (isBuiltin(name)) {
this.moduleType = "node"
} else if (isNodeModule(name, options)) {
this.moduleType = "npm"
} else if (name.startsWith("http://") || name.startsWith("https://")) {
this.moduleType = "http"
}
/**
* The full path of this import target.
* If the target is a module and it does not exist then this is `null`.
* @type {string|null}
*/
this.filePath = getFilePath(isModule, name, options, moduleType)
/**
* The module name of this import target.
* If the target is a relative path then this is `null`.
* @type {string|null}
*/
this.moduleName = isModule ? getModuleName(name) : null
}
}

View file

@ -0,0 +1,21 @@
"use strict"
const path = require("path")
const typescriptExtensions = [".ts", ".tsx", ".cts", ".mts"]
/**
* Determine if the context source file is typescript.
*
* @param {RuleContext} context - A context
* @returns {boolean}
*/
module.exports = function isTypescript(context) {
const sourceFileExt = path.extname(
context.physicalFilename ??
context.getPhysicalFilename?.() ??
context.filename ??
context.getFilename?.()
)
return typescriptExtensions.includes(sourceFileExt)
}

View file

@ -0,0 +1,40 @@
"use strict"
const path = require("path")
const isTypescript = require("./is-typescript")
const getTypescriptExtensionMap = require("./get-typescript-extension-map")
/**
* Maps the typescript file extension that should be added in an import statement,
* based on the given file extension of the referenced file OR fallsback to the original given extension.
*
* For example, in typescript, when referencing another typescript from a typescript file,
* a .js extension should be used instead of the original .ts extension of the referenced file.
*
* @param {import('eslint').Rule.RuleContext} context
* @param {string} filePath The filePath of the import
* @param {string} fallbackExtension The non-typescript fallback
* @param {boolean} reverse Execute a reverse path mapping
* @returns {string} The file extension to append to the import statement.
*/
module.exports = function mapTypescriptExtension(
context,
filePath,
fallbackExtension,
reverse = false
) {
const { forward, backward } = getTypescriptExtensionMap(context)
const ext = path.extname(filePath)
if (reverse) {
if (isTypescript(context) && ext in backward) {
return backward[ext]
}
return [fallbackExtension]
} else {
if (isTypescript(context) && ext in forward) {
return forward[ext]
}
}
return fallbackExtension
}

View file

@ -0,0 +1,46 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* See LICENSE file in root directory for full license.
*/
"use strict"
/**
* Merge two visitors.
* This function modifies `visitor1` directly to merge.
* @param {Visitor} visitor1 The visitor which is assigned.
* @param {Visitor} visitor2 The visitor which is assigning.
* @returns {Visitor} `visitor1`.
*/
module.exports = function mergeVisitorsInPlace(visitor1, visitor2) {
for (const key of Object.keys(visitor2)) {
const handler1 = visitor1[key]
const handler2 = visitor2[key]
if (typeof handler1 === "function") {
if (handler1._handlers) {
handler1._handlers.push(handler2)
} else {
const handlers = [handler1, handler2]
visitor1[key] = Object.assign(dispatch.bind(null, handlers), {
_handlers: handlers,
})
}
} else {
visitor1[key] = handler2
}
}
return visitor1
}
/**
* Dispatch all given functions with a node.
* @param {function[]} handlers The function list to call.
* @param {Node} node The AST node to be handled.
* @returns {void}
*/
function dispatch(handlers, node) {
for (const h of handlers) {
h(node)
}
}

View file

@ -0,0 +1,10 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
module.exports = function stripImportPathParams(path) {
const i = path.indexOf("!")
return i === -1 ? path : path.slice(0, i)
}

View file

@ -0,0 +1,13 @@
"use strict"
/**
* Remove `node:` prefix from module name
* @param {string} name The module name such as `node:assert` or `assert`.
* @returns {string} The unprefixed module name like `assert`.
*/
module.exports = function unprefixNodeColon(name) {
if (name.startsWith("node:")) {
return name.slice(5)
}
return name
}

View file

@ -0,0 +1,80 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const isCoreModule = require("is-core-module")
const getResolvePaths = require("./get-resolve-paths")
const getTryExtensions = require("./get-try-extensions")
const ImportTarget = require("./import-target")
const stripImportPathParams = require("./strip-import-path-params")
/**
* Gets a list of `import`/`export` declaration targets.
*
* Core modules of Node.js (e.g. `fs`, `http`) are excluded.
*
* @param {RuleContext} context - The rule context.
* @param {Object} [options] - The flag to include core modules.
* @param {boolean} [options.includeCore] - The flag to include core modules.
* @param {number} [options.optionIndex] - The index of rule options.
* @param {boolean} [options.ignoreTypeImport] - The flag to ignore typescript type imports.
* @param {function(ImportTarget[]):void} callback The callback function to get result.
* @returns {ImportTarget[]} A list of found target's information.
*/
module.exports = function visitImport(
context,
{ includeCore = false, optionIndex = 0, ignoreTypeImport = false } = {},
callback
) {
const targets = []
const basedir = path.dirname(
path.resolve(context.filename ?? context.getFilename())
)
const paths = getResolvePaths(context, optionIndex)
const extensions = getTryExtensions(context, optionIndex)
const options = { basedir, paths, extensions }
return {
[[
"ExportAllDeclaration",
"ExportNamedDeclaration",
"ImportDeclaration",
"ImportExpression",
]](node) {
const sourceNode = node.source
// skip `import(foo)`
if (
node.type === "ImportExpression" &&
sourceNode &&
sourceNode.type !== "Literal"
) {
return
}
// skip `import type { foo } from 'bar'` (for eslint-typescript)
if (
ignoreTypeImport &&
node.type === "ImportDeclaration" &&
node.importKind === "type"
) {
return
}
const name = sourceNode && stripImportPathParams(sourceNode.value)
// Note: "999" arbitrary to check current/future Node.js version
if (name && (includeCore || !isCoreModule(name, "999"))) {
targets.push(
new ImportTarget(sourceNode, name, options, "import")
)
}
},
"Program:exit"() {
callback(targets)
},
}
}

View file

@ -0,0 +1,71 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
"use strict"
const path = require("path")
const {
CALL,
ReferenceTracker,
getStringIfConstant,
} = require("@eslint-community/eslint-utils")
const isCoreModule = require("is-core-module")
const getResolvePaths = require("./get-resolve-paths")
const getTryExtensions = require("./get-try-extensions")
const ImportTarget = require("./import-target")
const stripImportPathParams = require("./strip-import-path-params")
/**
* Gets a list of `require()` targets.
*
* Core modules of Node.js (e.g. `fs`, `http`) are excluded.
*
* @param {RuleContext} context - The rule context.
* @param {Object} [options] - The flag to include core modules.
* @param {boolean} [options.includeCore] - The flag to include core modules.
* @param {function(ImportTarget[]):void} callback The callback function to get result.
* @returns {Object} The visitor.
*/
module.exports = function visitRequire(
context,
{ includeCore = false } = {},
callback
) {
const targets = []
const basedir = path.dirname(
path.resolve(context.filename ?? context.getFilename())
)
const paths = getResolvePaths(context)
const extensions = getTryExtensions(context)
const options = { basedir, paths, extensions }
return {
"Program:exit"(node) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
const tracker = new ReferenceTracker(
sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9
)
const references = tracker.iterateGlobalReferences({
require: {
[CALL]: true,
resolve: { [CALL]: true },
},
})
for (const { node } of references) {
const targetNode = node.arguments[0]
const rawName = getStringIfConstant(targetNode)
const name = rawName && stripImportPathParams(rawName)
// Note: "999" arbitrary to check current/future Node.js version
if (name && (includeCore || !isCoreModule(name, "999"))) {
targets.push(
new ImportTarget(targetNode, name, options, "require")
)
}
}
callback(targets)
},
}
}