tools: update ESLint to v9 and use flat config

Closes: https://github.com/nodejs/node/issues/52567
PR-URL: https://github.com/nodejs/node/pull/52780
Fixes: https://github.com/nodejs/node/issues/52567
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Michaël Zasso 2024-05-23 21:45:18 +02:00 committed by GitHub
parent b9ad94b6da
commit 7e6d92c485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1255 changed files with 16091 additions and 29665 deletions

View file

@ -1,15 +0,0 @@
node_modules
lib/punycode.js
test/addons/??_*
test/fixtures
test/message/esm_display_syntax_error.mjs
tools/icu
tools/lint-md/lint-md.mjs
tools/github_reporter
benchmark/tmp
benchmark/fixtures
doc/**/*.js
doc/changelogs/CHANGELOG_v1*.md
!doc/changelogs/CHANGELOG_v18.md
!doc/api_assets/*.js
!.eslintrc.js

View file

@ -1,375 +0,0 @@
'use strict';
/* eslint-env node */
const Module = require('module');
const path = require('path');
const NodePlugin = require('./tools/node_modules/eslint-plugin-node-core');
NodePlugin.RULES_DIR = path.resolve(__dirname, 'tools', 'eslint-rules');
// The Module._findPath() monkeypatching is to make it so that ESLint will work
// if invoked by a globally-installed ESLint or ESLint installed elsewhere
// rather than the one we ship. This makes it possible for IDEs to lint files
// with our rules while people edit them.
const ModuleFindPath = Module._findPath;
const hacks = [
'eslint-plugin-node-core',
'eslint-plugin-jsdoc',
'eslint-plugin-markdown',
'@babel/eslint-parser',
'@babel/plugin-syntax-import-attributes',
'@stylistic/eslint-plugin-js',
];
Module._findPath = (request, paths, isMain) => {
const r = ModuleFindPath(request, paths, isMain);
if (!r && hacks.includes(request)) {
try {
return require.resolve(`./tools/node_modules/${request}`);
} catch {
return require.resolve(
`./tools/node_modules/eslint/node_modules/${request}`);
}
}
return r;
};
module.exports = {
root: true,
env: {
es2022: true,
},
extends: ['eslint:recommended', 'plugin:jsdoc/recommended'],
plugins: ['jsdoc', 'markdown', 'node-core', '@stylistic/js'],
parser: '@babel/eslint-parser',
parserOptions: {
babelOptions: {
plugins: [
Module._findPath('@babel/plugin-syntax-import-attributes'),
],
},
requireConfigFile: false,
sourceType: 'script',
},
overrides: [
{
files: [
'*.mjs',
'test/es-module/test-esm-example-loader.js',
'test/es-module/test-esm-type-flag.js',
'test/es-module/test-esm-type-flag-alias.js',
'test/es-module/test-require-module-detect-entry-point.js',
'test/es-module/test-require-module-detect-entry-point-aou.js',
],
parserOptions: { sourceType: 'module' },
},
{
files: ['**/*.md'],
processor: 'markdown/markdown',
},
{
files: ['**/*.md/*.cjs', '**/*.md/*.js'],
parserOptions: {
sourceType: 'script',
ecmaFeatures: { impliedStrict: true },
},
rules: { strict: 'off' },
},
{
files: [
'**/*.md/*.mjs',
'doc/api/esm.md/*.js',
'doc/api/packages.md/*.js',
],
parserOptions: { sourceType: 'module' },
rules: { 'no-restricted-globals': [
'error',
{
name: '__filename',
message: 'Use import.meta.url instead',
},
{
name: '__dirname',
message: 'Not available in ESM',
},
{
name: 'exports',
message: 'Not available in ESM',
},
{
name: 'module',
message: 'Not available in ESM',
},
{
name: 'require',
message: 'Use import instead',
},
{
name: 'Buffer',
message: 'Import Buffer instead of using the global',
},
{
name: 'process',
message: 'Import process instead of using the global',
},
] },
},
{
files: [
'lib/internal/modules/**/*.js',
],
rules: {
'curly': 'error',
},
},
{
files: [
'lib/internal/test_runner/**/*.js',
],
rules: {
'node-core/set-proto-to-null-in-object': 'error',
},
},
],
rules: {
// ESLint built-in rules
// https://eslint.org/docs/rules/
'accessor-pairs': 'error',
'array-callback-return': 'error',
'block-scoped-var': 'error',
'capitalized-comments': ['error', 'always', {
line: {
// Ignore all lines that have less characters than 20 and all lines that
// start with something that looks like a variable name or code.
ignorePattern: '.{0,20}$|[a-z]+ ?[0-9A-Z_.(/=:[#-]|std|http|ssh|ftp',
ignoreInlineComments: true,
ignoreConsecutiveComments: true,
},
block: {
ignorePattern: '.*',
},
}],
'default-case-last': 'error',
'dot-notation': 'error',
'eqeqeq': ['error', 'smart'],
'func-name-matching': 'error',
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-constant-condition': ['error', { checkLoops: false }],
'no-constructor-return': 'error',
'no-duplicate-imports': 'error',
'no-else-return': 'error',
'no-lonely-if': 'error',
'no-mixed-requires': 'error',
'no-new-require': 'error',
'no-path-concat': 'error',
'no-proto': 'error',
'no-redeclare': ['error', { 'builtinGlobals': false }],
'no-restricted-modules': ['error', 'sys'],
'no-restricted-properties': [
'error',
{
object: 'assert',
property: 'deepEqual',
message: 'Use `assert.deepStrictEqual()`.',
},
{
object: 'assert',
property: 'notDeepEqual',
message: 'Use `assert.notDeepStrictEqual()`.',
},
{
object: 'assert',
property: 'equal',
message: 'Use `assert.strictEqual()` rather than `assert.equal()`.',
},
{
object: 'assert',
property: 'notEqual',
message: 'Use `assert.notStrictEqual()` rather than `assert.notEqual()`.',
},
{
property: '__defineGetter__',
message: '__defineGetter__ is deprecated.',
},
{
property: '__defineSetter__',
message: '__defineSetter__ is deprecated.',
},
],
// If this list is modified, please copy changes that should apply to ./lib
// as well to lib/.eslintrc.yaml.
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.name='setTimeout'][arguments.length<2]",
message: '`setTimeout()` must be invoked with at least two arguments.',
},
{
selector: "CallExpression[callee.name='setInterval'][arguments.length<2]",
message: '`setInterval()` must be invoked with at least two arguments.',
},
{
selector: 'ThrowStatement > CallExpression[callee.name=/Error$/]',
message: 'Use `new` keyword when throwing an `Error`.',
},
{
selector: "CallExpression[callee.property.name='substr']",
message: 'Use String.prototype.slice() or String.prototype.substring() instead of String.prototype.substr()',
},
{
selector: "CallExpression[callee.name='isNaN']",
message: 'Use Number.isNaN() instead of the global isNaN() function.',
},
{
// TODO(@panva): move this to no-restricted-properties
// when https://github.com/eslint/eslint/issues/16412 is fixed
selector: "Identifier[name='webcrypto']",
message: 'Use `globalThis.crypto`.',
},
],
'no-self-compare': 'error',
'no-template-curly-in-string': 'error',
'no-throw-literal': 'error',
'no-undef': ['error', { typeof: true }],
'no-undef-init': 'error',
'no-unused-expressions': ['error', { allowShortCircuit: true }],
'no-unused-vars': ['error', { args: 'none', caughtErrors: 'all' }],
'no-use-before-define': ['error', {
classes: true,
functions: false,
variables: false,
}],
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-useless-constructor': 'error',
'no-useless-return': 'error',
'no-var': 'error',
'no-void': 'error',
'one-var': ['error', { initialized: 'never' }],
'prefer-const': ['error', { ignoreReadBeforeAssign: true }],
'prefer-object-has-own': 'error',
'strict': ['error', 'global'],
'symbol-description': 'error',
'unicode-bom': 'error',
'valid-typeof': ['error', { requireStringLiterals: true }],
// ESLint recommended rules that we disable
'no-inner-declarations': 'off',
// JSDoc recommended rules that we disable
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param-description': 'off',
'jsdoc/newline-after-description': 'off',
'jsdoc/require-returns-description': 'off',
'jsdoc/valid-types': 'off',
'jsdoc/no-defaults': 'off',
'jsdoc/no-undefined-types': 'off',
'jsdoc/require-param': 'off',
'jsdoc/check-tag-names': 'off',
'jsdoc/require-returns': 'off',
// Stylistic rules
'@stylistic/js/arrow-parens': 'error',
'@stylistic/js/arrow-spacing': 'error',
'@stylistic/js/block-spacing': 'error',
'@stylistic/js/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/js/comma-dangle': ['error', 'always-multiline'],
'@stylistic/js/comma-spacing': 'error',
'@stylistic/js/comma-style': 'error',
'@stylistic/js/computed-property-spacing': 'error',
'@stylistic/js/dot-location': ['error', 'property'],
'@stylistic/js/eol-last': 'error',
'@stylistic/js/func-call-spacing': 'error',
'@stylistic/js/indent': ['error', 2, {
ArrayExpression: 'first',
CallExpression: { arguments: 'first' },
FunctionDeclaration: { parameters: 'first' },
FunctionExpression: { parameters: 'first' },
MemberExpression: 'off',
ObjectExpression: 'first',
SwitchCase: 1,
}],
'@stylistic/js/key-spacing': 'error',
'@stylistic/js/keyword-spacing': 'error',
'@stylistic/js/linebreak-style': 'error',
'@stylistic/js/max-len': ['error', {
code: 120,
ignorePattern: '^// Flags:',
ignoreRegExpLiterals: true,
ignoreTemplateLiterals: true,
ignoreUrls: true,
tabWidth: 2,
}],
'@stylistic/js/new-parens': 'error',
'@stylistic/js/no-confusing-arrow': 'error',
'@stylistic/js/no-extra-parens': ['error', 'functions'],
'@stylistic/js/no-multi-spaces': ['error', { ignoreEOLComments: true }],
'@stylistic/js/no-multiple-empty-lines': ['error', { max: 2, maxEOF: 0, maxBOF: 0 }],
'@stylistic/js/no-tabs': 'error',
'@stylistic/js/no-trailing-spaces': 'error',
'@stylistic/js/no-whitespace-before-property': 'error',
'@stylistic/js/object-curly-newline': 'error',
'@stylistic/js/object-curly-spacing': ['error', 'always'],
'@stylistic/js/one-var-declaration-per-line': 'error',
'@stylistic/js/operator-linebreak': ['error', 'after'],
'@stylistic/js/padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: 'function', next: 'function' },
],
'@stylistic/js/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/js/quote-props': ['error', 'consistent'],
'@stylistic/js/rest-spread-spacing': 'error',
'@stylistic/js/semi': 'error',
'@stylistic/js/semi-spacing': 'error',
'@stylistic/js/space-before-blocks': ['error', 'always'],
'@stylistic/js/space-before-function-paren': ['error', {
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
}],
'@stylistic/js/space-in-parens': 'error',
'@stylistic/js/space-infix-ops': 'error',
'@stylistic/js/space-unary-ops': 'error',
'@stylistic/js/spaced-comment': ['error', 'always', {
'block': { 'balanced': true },
'exceptions': ['-'],
}],
'@stylistic/js/template-curly-spacing': 'error',
// Custom rules from eslint-plugin-node-core
'node-core/no-unescaped-regexp-dot': 'error',
'node-core/no-duplicate-requires': 'error',
'node-core/prefer-proto': 'error',
},
globals: {
ByteLengthQueuingStrategy: 'readable',
CompressionStream: 'readable',
CountQueuingStrategy: 'readable',
CustomEvent: 'readable',
crypto: 'readable',
Crypto: 'readable',
CryptoKey: 'readable',
DecompressionStream: 'readable',
EventSource: 'readable',
fetch: 'readable',
FormData: 'readable',
navigator: 'readable',
ReadableStream: 'readable',
ReadableStreamDefaultReader: 'readable',
ReadableStreamBYOBReader: 'readable',
ReadableStreamBYOBRequest: 'readable',
ReadableByteStreamController: 'readable',
ReadableStreamDefaultController: 'readable',
Response: 'readable',
TextDecoderStream: 'readable',
TextEncoderStream: 'readable',
TransformStream: 'readable',
TransformStreamDefaultController: 'readable',
ShadowRealm: 'readable',
SubtleCrypto: 'readable',
WritableStream: 'readable',
WritableStreamDefaultWriter: 'readable',
WritableStreamDefaultController: 'readable',
WebSocket: 'readable',
},
};

View file

@ -66,7 +66,7 @@ subSystemLabels:
/^tools\/build-addons.mjs/: needs-ci
# All other tool changes should be marked as such
/^tools\//: tools
/^\.eslint|\.editorconfig/: tools
/^eslint\.config|\.editorconfig/: tools
/^typings\//: typings
## Dependencies

3
.gitignore vendored
View file

@ -13,9 +13,6 @@
!.clang-format
!.cpplint
!.editorconfig
!.eslintignore
!.eslintrc.js
!.eslintrc.yaml
!.gitattributes
!.github
!.gitignore

View file

@ -1194,7 +1194,7 @@ $(TARBALL): release-only doc-only
find $(TARNAME)/deps/v8/test/* -type d ! -regex '.*/test/torque$$' | xargs $(RM) -r
find $(TARNAME)/deps/v8/test -type f ! -regex '.*/test/torque/.*' | xargs $(RM)
find $(TARNAME)/deps/zlib/contrib/* -type d ! -regex '.*/contrib/optimizations$$' | xargs $(RM) -r
find $(TARNAME)/ -name ".eslint*" -maxdepth 2 | xargs $(RM)
find $(TARNAME)/ -name "eslint.config*" -maxdepth 2 | xargs $(RM)
find $(TARNAME)/ -type l | xargs $(RM)
tar -cf $(TARNAME).tar $(TARNAME)
$(RM) -r $(TARNAME)
@ -1372,7 +1372,7 @@ format-md:
LINT_JS_TARGETS = .eslintrc.js benchmark doc lib test tools
LINT_JS_TARGETS = eslint.config.mjs benchmark doc lib test tools
run-lint-js = tools/node_modules/eslint/bin/eslint.js --cache \
--max-warnings=0 --report-unused-disable-directives $(LINT_JS_TARGETS)

View file

@ -1,8 +0,0 @@
## Benchmark-specific linter rules
env:
node: true
es6: true
rules:
prefer-arrow-callback: error

View file

@ -0,0 +1,17 @@
import { requireEslintTool } from '../tools/eslint.config_utils.mjs';
const globals = requireEslintTool('globals');
export default [
{
files: ['benchmark/**/*.{js,mjs,cjs}'],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'prefer-arrow-callback': 'error',
},
},
];

View file

@ -1,17 +0,0 @@
## Docs-specific linter rules
rules:
# Ease some restrictions in doc examples
no-restricted-properties: off
no-undef: off
no-unused-expressions: off
no-unused-vars: off
symbol-description: off
# Add new ECMAScript features gradually
prefer-const: error
prefer-rest-params: error
prefer-template: error
# Stylistic rules
'@stylistic/js/no-multiple-empty-lines': [error, {max: 1, maxEOF: 0, maxBOF: 0}]

View file

@ -855,7 +855,7 @@ might impact an LTS release.
| `src/node_api.*` | @nodejs/node-api |
| `src/node_crypto.*`, `src/crypto` | @nodejs/crypto |
| `test/*` | @nodejs/testing |
| `tools/node_modules/eslint`, `.eslintrc` | @nodejs/linting |
| `tools/node_modules/eslint`, `eslint.config.mjs` | @nodejs/linting |
| build | @nodejs/build |
| `src/module_wrap.*`, `lib/internal/modules/*`, `lib/internal/vm/module.js` | @nodejs/modules |
| GYP | @nodejs/gyp |

View file

@ -0,0 +1,40 @@
import { requireEslintTool } from '../tools/eslint.config_utils.mjs';
const globals = requireEslintTool('globals');
export default [
{
files: ['doc/**/*.md/*.{js,mjs,cjs}'],
rules: {
// Ease some restrictions in doc examples.
'no-restricted-properties': 'off',
'no-undef': 'off',
'no-unused-expressions': 'off',
'no-unused-vars': 'off',
'symbol-description': 'off',
// Add new ECMAScript features gradually.
'prefer-const': 'error',
'prefer-rest-params': 'error',
'prefer-template': 'error',
// Stylistic rules.
'@stylistic/js/no-multiple-empty-lines': [
'error',
{
max: 1,
maxEOF: 0,
maxBOF: 0,
},
],
},
},
{
files: ['doc/api_assets/*.js'],
languageOptions: {
globals: {
...globals.browser,
},
},
},
];

383
eslint.config.mjs Normal file
View file

@ -0,0 +1,383 @@
import Module from 'node:module';
import { fileURLToPath, URL } from 'node:url';
import benchmarkConfig from './benchmark/eslint.config_partial.mjs';
import docConfig from './doc/eslint.config_partial.mjs';
import libConfig from './lib/eslint.config_partial.mjs';
import testConfig from './test/eslint.config_partial.mjs';
import toolsConfig from './tools/eslint.config_partial.mjs';
import {
noRestrictedSyntaxCommonAll,
noRestrictedSyntaxCommonLib,
noRestrictedSyntaxCommonTest,
requireEslintTool,
resolveEslintTool,
} from './tools/eslint.config_utils.mjs';
const js = requireEslintTool('@eslint/js');
const babelEslintParser = requireEslintTool('@babel/eslint-parser');
const babelPluginSyntaxImportAttributes = requireEslintTool('@babel/plugin-syntax-import-attributes');
const jsdoc = requireEslintTool('eslint-plugin-jsdoc');
const markdown = requireEslintTool('eslint-plugin-markdown');
const stylisticJs = requireEslintTool('@stylistic/eslint-plugin-js');
const nodeCore = requireEslintTool('eslint-plugin-node-core');
nodeCore.RULES_DIR = fileURLToPath(new URL('./tools/eslint-rules', import.meta.url));
// The Module._resolveFilename() monkeypatching is to make it so that ESLint is able to
// dynamically load extra modules that we install with it.
const ModuleResolveFilename = Module._resolveFilename;
const hacks = [
'eslint-formatter-tap',
];
Module._resolveFilename = (request, parent, isMain, options) => {
if (hacks.includes(request) && parent.id.endsWith('__placeholder__.js')) {
return resolveEslintTool(request);
}
return ModuleResolveFilename(request, parent, isMain, options);
};
export default [
// #region ignores
{
ignores: [
'**/node_modules/**',
'benchmark/fixtures/**',
'benchmark/tmp/**',
'doc/**/*.js',
'doc/changelogs/CHANGELOG_V1*.md',
'!doc/api_assets/*.js',
'!doc/changelogs/CHANGELOG_V18.md',
'lib/punycode.js',
'test/.tmp.*/**',
'test/addons/??_*',
'test/fixtures/**',
'test/message/esm_display_syntax_error.mjs',
'tools/github_reporter/**',
'tools/icu/**',
'tools/lint-md/lint-md.mjs',
],
},
// #endregion
// #region general config
js.configs.recommended,
jsdoc.configs['flat/recommended'],
{
files: ['**/*.{js,cjs}'],
languageOptions: {
// The default is `commonjs` but it's not supported by the Babel parser.
sourceType: 'script',
},
},
{
plugins: {
jsdoc,
'@stylistic/js': stylisticJs,
'node-core': nodeCore,
},
languageOptions: {
parser: babelEslintParser,
parserOptions: {
babelOptions: {
plugins: [
babelPluginSyntaxImportAttributes,
],
},
requireConfigFile: false,
},
},
},
// #endregion
// #region general globals
{
languageOptions: {
globals: {
ByteLengthQueuingStrategy: 'readonly',
CompressionStream: 'readonly',
CountQueuingStrategy: 'readonly',
CustomEvent: 'readonly',
crypto: 'readonly',
Crypto: 'readonly',
CryptoKey: 'readonly',
DecompressionStream: 'readonly',
EventSource: 'readable',
fetch: 'readonly',
FormData: 'readonly',
navigator: 'readonly',
ReadableStream: 'readonly',
ReadableStreamDefaultReader: 'readonly',
ReadableStreamBYOBReader: 'readonly',
ReadableStreamBYOBRequest: 'readonly',
ReadableByteStreamController: 'readonly',
ReadableStreamDefaultController: 'readonly',
Response: 'readonly',
TextDecoderStream: 'readonly',
TextEncoderStream: 'readonly',
TransformStream: 'readonly',
TransformStreamDefaultController: 'readonly',
ShadowRealm: 'readonly',
SubtleCrypto: 'readonly',
WritableStream: 'readonly',
WritableStreamDefaultWriter: 'readonly',
WritableStreamDefaultController: 'readonly',
WebSocket: 'readonly',
},
},
},
// #endregion
// #region general rules
{
rules: {
// ESLint built-in rules
// https://eslint.org/docs/latest/rules/
'accessor-pairs': 'error',
'array-callback-return': 'error',
'block-scoped-var': 'error',
'capitalized-comments': ['error', 'always', {
line: {
// Ignore all lines that have less characters than 20 and all lines
// that start with something that looks like a variable name or code.
ignorePattern: '.{0,20}$|[a-z]+ ?[0-9A-Z_.(/=:[#-]|std|http|ssh|ftp',
ignoreInlineComments: true,
ignoreConsecutiveComments: true,
},
block: {
ignorePattern: '.*',
},
}],
'default-case-last': 'error',
'dot-notation': 'error',
'eqeqeq': ['error', 'smart'],
'func-name-matching': 'error',
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'no-constant-condition': ['error', { checkLoops: false }],
'no-constructor-return': 'error',
'no-duplicate-imports': 'error',
'no-else-return': 'error',
'no-lonely-if': 'error',
'no-mixed-requires': 'error',
'no-new-require': 'error',
'no-path-concat': 'error',
'no-proto': 'error',
'no-redeclare': ['error', { builtinGlobals: false }],
'no-restricted-modules': ['error', 'sys'],
'no-restricted-properties': [
'error',
{
object: 'assert',
property: 'deepEqual',
message: 'Use `assert.deepStrictEqual()`.',
},
{
object: 'assert',
property: 'notDeepEqual',
message: 'Use `assert.notDeepStrictEqual()`.',
},
{
object: 'assert',
property: 'equal',
message: 'Use `assert.strictEqual()` rather than `assert.equal()`.',
},
{
object: 'assert',
property: 'notEqual',
message: 'Use `assert.notStrictEqual()` rather than `assert.notEqual()`.',
},
{
property: '__defineGetter__',
message: '__defineGetter__ is deprecated.',
},
{
property: '__defineSetter__',
message: '__defineSetter__ is deprecated.',
},
],
'no-restricted-syntax': [
'error',
...noRestrictedSyntaxCommonAll,
...noRestrictedSyntaxCommonLib,
...noRestrictedSyntaxCommonTest,
],
'no-self-compare': 'error',
'no-template-curly-in-string': 'error',
'no-throw-literal': 'error',
'no-undef': ['error', { typeof: true }],
'no-undef-init': 'error',
'no-unused-expressions': ['error', { allowShortCircuit: true }],
'no-unused-vars': ['error', { args: 'none', caughtErrors: 'all' }],
'no-use-before-define': ['error', {
classes: true,
functions: false,
variables: false,
}],
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-useless-constructor': 'error',
'no-useless-return': 'error',
'no-var': 'error',
'no-void': 'error',
'one-var': ['error', { initialized: 'never' }],
'prefer-const': ['error', { ignoreReadBeforeAssign: true }],
'prefer-object-has-own': 'error',
'strict': ['error', 'global'],
'symbol-description': 'error',
'unicode-bom': 'error',
'valid-typeof': ['error', { requireStringLiterals: true }],
// ESLint recommended rules that we disable.
'no-inner-declarations': 'off',
// JSDoc recommended rules that we disable.
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param-description': 'off',
'jsdoc/newline-after-description': 'off',
'jsdoc/require-returns-description': 'off',
'jsdoc/valid-types': 'off',
'jsdoc/no-defaults': 'off',
'jsdoc/no-undefined-types': 'off',
'jsdoc/require-param': 'off',
'jsdoc/check-tag-names': 'off',
'jsdoc/require-returns': 'off',
// Stylistic rules.
'@stylistic/js/arrow-parens': 'error',
'@stylistic/js/arrow-spacing': 'error',
'@stylistic/js/block-spacing': 'error',
'@stylistic/js/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/js/comma-dangle': ['error', 'always-multiline'],
'@stylistic/js/comma-spacing': 'error',
'@stylistic/js/comma-style': 'error',
'@stylistic/js/computed-property-spacing': 'error',
'@stylistic/js/dot-location': ['error', 'property'],
'@stylistic/js/eol-last': 'error',
'@stylistic/js/func-call-spacing': 'error',
'@stylistic/js/indent': ['error', 2, {
ArrayExpression: 'first',
CallExpression: { arguments: 'first' },
FunctionDeclaration: { parameters: 'first' },
FunctionExpression: { parameters: 'first' },
MemberExpression: 'off',
ObjectExpression: 'first',
SwitchCase: 1,
}],
'@stylistic/js/key-spacing': 'error',
'@stylistic/js/keyword-spacing': 'error',
'@stylistic/js/linebreak-style': 'error',
'@stylistic/js/max-len': ['error', {
code: 120,
ignorePattern: '^// Flags:',
ignoreRegExpLiterals: true,
ignoreTemplateLiterals: true,
ignoreUrls: true,
tabWidth: 2,
}],
'@stylistic/js/new-parens': 'error',
'@stylistic/js/no-confusing-arrow': 'error',
'@stylistic/js/no-extra-parens': ['error', 'functions'],
'@stylistic/js/no-multi-spaces': ['error', { ignoreEOLComments: true }],
'@stylistic/js/no-multiple-empty-lines': ['error', { max: 2, maxEOF: 0, maxBOF: 0 }],
'@stylistic/js/no-tabs': 'error',
'@stylistic/js/no-trailing-spaces': 'error',
'@stylistic/js/no-whitespace-before-property': 'error',
'@stylistic/js/object-curly-newline': 'error',
'@stylistic/js/object-curly-spacing': ['error', 'always'],
'@stylistic/js/one-var-declaration-per-line': 'error',
'@stylistic/js/operator-linebreak': ['error', 'after'],
'@stylistic/js/padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: 'function', next: 'function' },
],
'@stylistic/js/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/js/quote-props': ['error', 'consistent'],
'@stylistic/js/rest-spread-spacing': 'error',
'@stylistic/js/semi': 'error',
'@stylistic/js/semi-spacing': 'error',
'@stylistic/js/space-before-blocks': ['error', 'always'],
'@stylistic/js/space-before-function-paren': ['error', {
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
}],
'@stylistic/js/space-in-parens': 'error',
'@stylistic/js/space-infix-ops': 'error',
'@stylistic/js/space-unary-ops': 'error',
'@stylistic/js/spaced-comment': ['error', 'always', {
'block': { 'balanced': true },
'exceptions': ['-'],
}],
'@stylistic/js/template-curly-spacing': 'error',
// Custom rules in tools/eslint-rules.
'node-core/no-unescaped-regexp-dot': 'error',
'node-core/no-duplicate-requires': 'error',
'node-core/prefer-proto': 'error',
},
},
// #endregion
// #region markdown config
{
files: ['**/*.md'],
plugins: {
markdown,
},
processor: 'markdown/markdown',
},
{
files: ['**/*.md/*.{js,cjs}'],
languageOptions: {
parserOptions: {
ecmaFeatures: { impliedStrict: true },
},
},
rules: { strict: 'off' },
},
{
files: [
'**/*.md/*.mjs',
'doc/api/esm.md/*.js',
'doc/api/packages.md/*.js',
],
languageOptions: {
sourceType: 'module',
},
rules: { 'no-restricted-globals': [
'error',
{
name: '__filename',
message: 'Use import.meta.url instead',
},
{
name: '__dirname',
message: 'Not available in ESM',
},
{
name: 'exports',
message: 'Not available in ESM',
},
{
name: 'module',
message: 'Not available in ESM',
},
{
name: 'require',
message: 'Use import instead',
},
{
name: 'Buffer',
message: 'Import Buffer instead of using the global',
},
{
name: 'process',
message: 'Import process instead of using the global',
},
] },
},
// #endregion
// #region partials
...benchmarkConfig,
...docConfig,
...libConfig,
...testConfig,
...toolsConfig,
// #endregion
];

View file

@ -1,276 +0,0 @@
env:
es6: true
rules:
prefer-object-spread: error
no-buffer-constructor: error
no-restricted-syntax:
# Config copied from .eslintrc.js
- error
- selector: CallExpression[callee.object.name='assert']:not([callee.property.name='ok']):not([callee.property.name='fail']):not([callee.property.name='ifError'])
message: Please only use simple assertions in ./lib
- selector: CallExpression[callee.name='setTimeout'][arguments.length<2]
message: setTimeout() must be invoked with at least two arguments.
- selector: CallExpression[callee.name='setInterval'][arguments.length<2]
message: setInterval() must be invoked with at least 2 arguments.
- selector: ThrowStatement > CallExpression[callee.name=/Error$/]
message: Use new keyword when throwing an Error.
# Config specific to lib
- selector: NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])
message: Use an error exported by the internal/errors module.
- selector: CallExpression[callee.object.name='Error'][callee.property.name='captureStackTrace']
message: Please use `require('internal/errors').hideStackFrames()` instead.
- selector: AssignmentExpression:matches([left.object.name='Error']):matches([left.name='prepareStackTrace'], [left.property.name='prepareStackTrace'])
message: Use 'overrideStackTrace' from 'lib/internal/errors.js' instead of 'Error.prepareStackTrace'.
- selector: ThrowStatement > NewExpression[callee.name=/^ERR_[A-Z_]+$/] > ObjectExpression:first-child:not(:has([key.name='message']):has([key.name='code']):has([key.name='syscall']))
message: The context passed into SystemError constructor must have .code, .syscall and .message.
no-restricted-globals:
- error
- name: AbortController
message: Use `const { AbortController } = require('internal/abort_controller');` instead of the global.
- name: AbortSignal
message: Use `const { AbortSignal } = require('internal/abort_controller');` instead of the global.
- name: Blob
message: Use `const { Blob } = require('buffer');` instead of the global.
- name: BroadcastChannel
message: Use `const { BroadcastChannel } = require('internal/worker/io');` instead of the global.
- name: Buffer
message: Use `const { Buffer } = require('buffer');` instead of the global.
- name: ByteLengthQueuingStrategy
message: Use `const { ByteLengthQueuingStrategy } = require('internal/webstreams/queuingstrategies')` instead of the global.
- name: CompressionStream
message: Use `const { CompressionStream } = require('internal/webstreams/compression')` instead of the global.
- name: CountQueuingStrategy
message: Use `const { CountQueuingStrategy } = require('internal/webstreams/queuingstrategies')` instead of the global.
- name: CustomEvent
message: Use `const { CustomEvent } = require('internal/event_target');` instead of the global.
- name: DecompressionStream
message: Use `const { DecompressionStream } = require('internal/webstreams/compression')` instead of the global.
- name: DOMException
message: Use lazy function `const { lazyDOMExceptionClass } = require('internal/util');` instead of the global.
- name: Event
message: Use `const { Event } = require('internal/event_target');` instead of the global.
- name: EventTarget
message: Use `const { EventTarget } = require('internal/event_target');` instead of the global.
- name: File
message: Use `const { File } = require('buffer');` instead of the global.
- name: FormData
message: Use `const { FormData } = require('internal/deps/undici/undici');` instead of the global.
- name: Headers
message: Use `const { Headers } = require('internal/deps/undici/undici');` instead of the global.
# Intl is not available in primordials because it can be
# disabled with --without-intl build flag.
- name: Intl
message: Use `const { Intl } = globalThis;` instead of the global.
- name: Iterator
message: Use `const { Iterator } = globalThis;` instead of the global.
- name: MessageChannel
message: Use `const { MessageChannel } = require('internal/worker/io');` instead of the global.
- name: MessageEvent
message: Use `const { MessageEvent } = require('internal/deps/undici/undici');` instead of the global.
- name: MessagePort
message: Use `const { MessagePort } = require('internal/worker/io');` instead of the global.
- name: Navigator
message: Use `const { Navigator } = require('internal/navigator');` instead of the global.
- name: navigator
message: Use `const { navigator } = require('internal/navigator');` instead of the global.
- name: PerformanceEntry
message: Use `const { PerformanceEntry } = require('perf_hooks');` instead of the global.
- name: PerformanceMark
message: Use `const { PerformanceMark } = require('perf_hooks');` instead of the global.
- name: PerformanceMeasure
message: Use `const { PerformanceMeasure } = require('perf_hooks');` instead of the global.
- name: PerformanceObserverEntryList
message: Use `const { PerformanceObserverEntryList } = require('perf_hooks');` instead of the global.
- name: PerformanceObserver
message: Use `const { PerformanceObserver } = require('perf_hooks');` instead of the global.
- name: PerformanceResourceTiming
message: Use `const { PerformanceResourceTiming } = require('perf_hooks');` instead of the global.
- name: ReadableStream
message: Use `const { ReadableStream } = require('internal/webstreams/readablestream')` instead of the global.
- name: ReadableStreamDefaultReader
message: Use `const { ReadableStreamDefaultReader } = require('internal/webstreams/readablestream')` instead of the global.
- name: ReadableStreamBYOBReader
message: Use `const { ReadableStreamBYOBReader } = require('internal/webstreams/readablestream')` instead of the global.
- name: ReadableStreamBYOBRequest
message: Use `const { ReadableStreamBYOBRequest } = require('internal/webstreams/readablestream')` instead of the global.
- name: ReadableByteStreamController
message: Use `const { ReadableByteStreamController } = require('internal/webstreams/readablestream')` instead of the global.
- name: ReadableStreamDefaultController
message: Use `const { ReadableStreamDefaultController } = require('internal/webstreams/readablestream')` instead of the global.
- name: Request
message: Use `const { Request } = require('internal/deps/undici/undici');` instead of the global.
- name: Response
message: Use `const { Response } = require('internal/deps/undici/undici');` instead of the global.
# ShadowRealm is not available in primordials because it can be
# disabled with --no-harmony-shadow-realm CLI flag.
- name: ShadowRealm
message: Use `const { ShadowRealm } = globalThis;` instead of the global.
# SharedArrayBuffer is not available in primordials because it can be
# disabled with --no-harmony-sharedarraybuffer CLI flag.
- name: SharedArrayBuffer
message: Use `const { SharedArrayBuffer } = globalThis;` instead of the global.
- name: TextDecoder
message: Use `const { TextDecoder } = require('internal/encoding');` instead of the global.
- name: TextDecoderStream
message: Use `const { TextDecoderStream } = require('internal/webstreams/encoding')` instead of the global.
- name: TextEncoder
message: Use `const { TextEncoder } = require('internal/encoding');` instead of the global.
- name: TextEncoderStream
message: Use `const { TextEncoderStream } = require('internal/webstreams/encoding')` instead of the global.
- name: TransformStream
message: Use `const { TransformStream } = require('internal/webstreams/transformstream')` instead of the global.
- name: TransformStreamDefaultController
message: Use `const { TransformStreamDefaultController } = require('internal/webstreams/transformstream')` instead of the global.
- name: URL
message: Use `const { URL } = require('internal/url');` instead of the global.
- name: URLSearchParams
message: Use `const { URLSearchParams } = require('internal/url');` instead of the global.
# WebAssembly is not available in primordials because it can be
# disabled with --jitless CLI flag.
- name: WebAssembly
message: Use `const { WebAssembly } = globalThis;` instead of the global.
- name: WritableStream
message: Use `const { WritableStream } = require('internal/webstreams/writablestream')` instead of the global.
- name: WritableStreamDefaultWriter
message: Use `const { WritableStreamDefaultWriter } = require('internal/webstreams/writablestream')` instead of the global.
- name: WritableStreamDefaultController
message: Use `const { WritableStreamDefaultController } = require('internal/webstreams/writablestream')` instead of the global.
- name: atob
message: Use `const { atob } = require('buffer');` instead of the global.
- name: btoa
message: Use `const { btoa } = require('buffer');` instead of the global.
- name: clearImmediate
message: Use `const { clearImmediate } = require('timers');` instead of the global.
- name: clearInterval
message: Use `const { clearInterval } = require('timers');` instead of the global.
- name: clearTimeout
message: Use `const { clearTimeout } = require('timers');` instead of the global.
- name: console
message: Use `const console = require('internal/console/global');` instead of the global.
- name: crypto
message: Use `const { crypto } = require('internal/crypto/webcrypto');` instead of the global.
- name: Crypto
message: Use `const { Crypto } = require('internal/crypto/webcrypto');` instead of the global.
- name: CryptoKey
message: Use `const { CryptoKey } = require('internal/crypto/webcrypto');` instead of the global.
- name: EventSource
message: Use `const { EventSource } = require('internal/deps/undici/undici');` instead of the global.
- name: fetch
message: Use `const { fetch } = require('internal/deps/undici/undici');` instead of the global.
- name: global
message: Use `const { globalThis } = primordials;` instead of `global`.
- name: globalThis
message: Use `const { globalThis } = primordials;` instead of the global.
- name: performance
message: Use `const { performance } = require('perf_hooks');` instead of the global.
- name: queueMicrotask
message: Use `const { queueMicrotask } = require('internal/process/task_queues');` instead of the global.
- name: setImmediate
message: Use `const { setImmediate } = require('timers');` instead of the global.
- name: setInterval
message: Use `const { setInterval } = require('timers');` instead of the global.
- name: setTimeout
message: Use `const { setTimeout } = require('timers');` instead of the global.
- name: structuredClone
message: Use `const { structuredClone } = internalBinding('messaging');` instead of the global.
- name: SubtleCrypto
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
no-restricted-modules:
- error
- name: url
message: Require `internal/url` instead of `url`.
# Stylistic rules
'@stylistic/js/no-mixed-operators':
- error
- groups: [['&&', '||']]
# Custom rules in tools/eslint-rules
node-core/alphabetize-errors: error
node-core/alphabetize-primordials: error
node-core/avoid-prototype-pollution: error
node-core/lowercase-name-for-primitive: error
node-core/non-ascii-character: error
node-core/no-array-destructuring: error
node-core/prefer-primordials:
- error
- name: AggregateError
- name: Array
- name: ArrayBuffer
- name: Atomics
- name: BigInt
- name: BigInt64Array
- name: BigUint64Array
- name: Boolean
- name: DataView
- name: Date
- name: decodeURI
- name: decodeURIComponent
- name: encodeURI
- name: encodeURIComponent
- name: escape
- name: eval
- name: Error
ignore:
- prepareStackTrace
- stackTraceLimit
- name: EvalError
- name: FinalizationRegistry
into: Safe
- name: Float32Array
- name: Float64Array
- name: Function
- name: Int16Array
- name: Int32Array
- name: Int8Array
- name: isFinite
into: Number
- name: isNaN
into: Number
- name: JSON
- name: Map
into: Safe
- name: Math
- name: Number
- name: Object
- name: parseFloat
into: Number
- name: parseInt
into: Number
- name: Proxy
- name: Promise
- name: RangeError
- name: ReferenceError
- name: Reflect
- name: RegExp
- name: Set
into: Safe
- name: String
- name: Symbol
- name: SyntaxError
- name: TypeError
- name: Uint16Array
- name: Uint32Array
- name: Uint8Array
- name: Uint8ClampedArray
- name: unescape
- name: URIError
- name: WeakMap
into: Safe
- name: WeakRef
into: Safe
- name: WeakSet
into: Safe
globals:
# Parameters passed to internal modules
require: false
process: false
exports: false
module: false
internalBinding: false
primordials: false
overrides:
- files: [internal/per_context/primordials.js]
rules:
node-core/alphabetize-primordials: [error, {enforceTopPosition: false}]

View file

@ -0,0 +1,486 @@
/* eslint-disable @stylistic/js/max-len */
import {
noRestrictedSyntaxCommonAll,
noRestrictedSyntaxCommonLib,
} from '../tools/eslint.config_utils.mjs';
export default [
{
files: ['lib/**/*.js'],
languageOptions: {
globals: {
// Parameters passed to internal modules.
require: 'readonly',
process: 'readonly',
exports: 'readonly',
module: 'readonly',
internalBinding: 'readonly',
primordials: 'readonly',
},
},
rules: {
'prefer-object-spread': 'error',
'no-buffer-constructor': 'error',
'no-restricted-syntax': [
'error',
...noRestrictedSyntaxCommonAll,
...noRestrictedSyntaxCommonLib,
{
selector: "CallExpression[callee.object.name='assert']:not([callee.property.name='ok']):not([callee.property.name='fail']):not([callee.property.name='ifError'])",
message: 'Please only use simple assertions in ./lib',
},
{
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError)$/])',
message: 'Use an error exported by the internal/errors module.',
},
{
selector: "CallExpression[callee.object.name='Error'][callee.property.name='captureStackTrace']",
message: "Please use `require('internal/errors').hideStackFrames()` instead.",
},
{
selector: "AssignmentExpression:matches([left.object.name='Error']):matches([left.name='prepareStackTrace'], [left.property.name='prepareStackTrace'])",
message: "Use 'overrideStackTrace' from 'lib/internal/errors.js' instead of 'Error.prepareStackTrace'.",
},
{
selector: "ThrowStatement > NewExpression[callee.name=/^ERR_[A-Z_]+$/] > ObjectExpression:first-child:not(:has([key.name='message']):has([key.name='code']):has([key.name='syscall']))",
message: 'The context passed into SystemError constructor must have .code, .syscall and .message.',
},
],
'no-restricted-globals': [
'error',
{
name: 'AbortController',
message: "Use `const { AbortController } = require('internal/abort_controller');` instead of the global.",
},
{
name: 'AbortSignal',
message: "Use `const { AbortSignal } = require('internal/abort_controller');` instead of the global.",
},
{
name: 'Blob',
message: "Use `const { Blob } = require('buffer');` instead of the global.",
},
{
name: 'BroadcastChannel',
message: "Use `const { BroadcastChannel } = require('internal/worker/io');` instead of the global.",
},
{
name: 'Buffer',
message: "Use `const { Buffer } = require('buffer');` instead of the global.",
},
{
name: 'ByteLengthQueuingStrategy',
message: "Use `const { ByteLengthQueuingStrategy } = require('internal/webstreams/queuingstrategies')` instead of the global.",
},
{
name: 'CompressionStream',
message: "Use `const { CompressionStream } = require('internal/webstreams/compression')` instead of the global.",
},
{
name: 'CountQueuingStrategy',
message: "Use `const { CountQueuingStrategy } = require('internal/webstreams/queuingstrategies')` instead of the global.",
},
{
name: 'CustomEvent',
message: "Use `const { CustomEvent } = require('internal/event_target');` instead of the global.",
},
{
name: 'DecompressionStream',
message: "Use `const { DecompressionStream } = require('internal/webstreams/compression')` instead of the global.",
},
{
name: 'DOMException',
message: "Use lazy function `const { lazyDOMExceptionClass } = require('internal/util');` instead of the global.",
},
{
name: 'Event',
message: "Use `const { Event } = require('internal/event_target');` instead of the global.",
},
{
name: 'EventTarget',
message: "Use `const { EventTarget } = require('internal/event_target');` instead of the global.",
},
{
name: 'File',
message: "Use `const { File } = require('buffer');` instead of the global.",
},
{
name: 'FormData',
message: "Use `const { FormData } = require('internal/deps/undici/undici');` instead of the global.",
},
{
name: 'Headers',
message: "Use `const { Headers } = require('internal/deps/undici/undici');` instead of the global.",
},
// Intl is not available in primordials because it can be
// disabled with --without-intl build flag.
{
name: 'Intl',
message: 'Use `const { Intl } = globalThis;` instead of the global.',
},
{
name: 'Iterator',
message: 'Use `const { Iterator } = globalThis;` instead of the global.',
},
{
name: 'MessageChannel',
message: "Use `const { MessageChannel } = require('internal/worker/io');` instead of the global.",
},
{
name: 'MessageEvent',
message: "Use `const { MessageEvent } = require('internal/deps/undici/undici');` instead of the global.",
},
{
name: 'MessagePort',
message: "Use `const { MessagePort } = require('internal/worker/io');` instead of the global.",
},
{
name: 'Navigator',
message: "Use `const { Navigator } = require('internal/navigator');` instead of the global.",
},
{
name: 'navigator',
message: "Use `const { navigator } = require('internal/navigator');` instead of the global.",
},
{
name: 'PerformanceEntry',
message: "Use `const { PerformanceEntry } = require('perf_hooks');` instead of the global.",
},
{
name: 'PerformanceMark',
message: "Use `const { PerformanceMark } = require('perf_hooks');` instead of the global.",
},
{
name: 'PerformanceMeasure',
message: "Use `const { PerformanceMeasure } = require('perf_hooks');` instead of the global.",
},
{
name: 'PerformanceObserverEntryList',
message: "Use `const { PerformanceObserverEntryList } = require('perf_hooks');` instead of the global.",
},
{
name: 'PerformanceObserver',
message: "Use `const { PerformanceObserver } = require('perf_hooks');` instead of the global.",
},
{
name: 'PerformanceResourceTiming',
message: "Use `const { PerformanceResourceTiming } = require('perf_hooks');` instead of the global.",
},
{
name: 'ReadableStream',
message: "Use `const { ReadableStream } = require('internal/webstreams/readablestream')` instead of the global.",
},
{
name: 'ReadableStreamDefaultReader',
message: "Use `const { ReadableStreamDefaultReader } = require('internal/webstreams/readablestream')` instead of the global.",
},
{
name: 'ReadableStreamBYOBReader',
message: "Use `const { ReadableStreamBYOBReader } = require('internal/webstreams/readablestream')` instead of the global.",
},
{
name: 'ReadableStreamBYOBRequest',
message: "Use `const { ReadableStreamBYOBRequest } = require('internal/webstreams/readablestream')` instead of the global.",
},
{
name: 'ReadableByteStreamController',
message: "Use `const { ReadableByteStreamController } = require('internal/webstreams/readablestream')` instead of the global.",
},
{
name: 'ReadableStreamDefaultController',
message: "Use `const { ReadableStreamDefaultController } = require('internal/webstreams/readablestream')` instead of the global.",
},
{
name: 'Request',
message: "Use `const { Request } = require('internal/deps/undici/undici');` instead of the global.",
},
{
name: 'Response',
message: "Use `const { Response } = require('internal/deps/undici/undici');` instead of the global.",
},
// ShadowRealm is not available in primordials because it can be
// disabled with --no-harmony-shadow-realm CLI flag.
{
name: 'ShadowRealm',
message: 'Use `const { ShadowRealm } = globalThis;` instead of the global.',
},
// SharedArrayBuffer is not available in primordials because it can be
// disabled with --no-harmony-sharedarraybuffer CLI flag.
{
name: 'SharedArrayBuffer',
message: 'Use `const { SharedArrayBuffer } = globalThis;` instead of the global.',
},
{
name: 'TextDecoder',
message: "Use `const { TextDecoder } = require('internal/encoding');` instead of the global.",
},
{
name: 'TextDecoderStream',
message: "Use `const { TextDecoderStream } = require('internal/webstreams/encoding')` instead of the global.",
},
{
name: 'TextEncoder',
message: "Use `const { TextEncoder } = require('internal/encoding');` instead of the global.",
},
{
name: 'TextEncoderStream',
message: "Use `const { TextEncoderStream } = require('internal/webstreams/encoding')` instead of the global.",
},
{
name: 'TransformStream',
message: "Use `const { TransformStream } = require('internal/webstreams/transformstream')` instead of the global.",
},
{
name: 'TransformStreamDefaultController',
message: "Use `const { TransformStreamDefaultController } = require('internal/webstreams/transformstream')` instead of the global.",
},
{
name: 'URL',
message: "Use `const { URL } = require('internal/url');` instead of the global.",
},
{
name: 'URLSearchParams',
message: "Use `const { URLSearchParams } = require('internal/url');` instead of the global.",
},
// WebAssembly is not available in primordials because it can be
// disabled with --jitless CLI flag.
{
name: 'WebAssembly',
message: 'Use `const { WebAssembly } = globalThis;` instead of the global.',
},
{
name: 'WritableStream',
message: "Use `const { WritableStream } = require('internal/webstreams/writablestream')` instead of the global.",
},
{
name: 'WritableStreamDefaultWriter',
message: "Use `const { WritableStreamDefaultWriter } = require('internal/webstreams/writablestream')` instead of the global.",
},
{
name: 'WritableStreamDefaultController',
message: "Use `const { WritableStreamDefaultController } = require('internal/webstreams/writablestream')` instead of the global.",
},
{
name: 'atob',
message: "Use `const { atob } = require('buffer');` instead of the global.",
},
{
name: 'btoa',
message: "Use `const { btoa } = require('buffer');` instead of the global.",
},
{
name: 'clearImmediate',
message: "Use `const { clearImmediate } = require('timers');` instead of the global.",
},
{
name: 'clearInterval',
message: "Use `const { clearInterval } = require('timers');` instead of the global.",
},
{
name: 'clearTimeout',
message: "Use `const { clearTimeout } = require('timers');` instead of the global.",
},
{
name: 'console',
message: "Use `const console = require('internal/console/global');` instead of the global.",
},
{
name: 'crypto',
message: "Use `const { crypto } = require('internal/crypto/webcrypto');` instead of the global.",
},
{
name: 'Crypto',
message: "Use `const { Crypto } = require('internal/crypto/webcrypto');` instead of the global.",
},
{
name: 'CryptoKey',
message: "Use `const { CryptoKey } = require('internal/crypto/webcrypto');` instead of the global.",
},
{
name: 'EventSource',
message: "Use `const { EventSource } = require('internal/deps/undici/undici');` instead of the global.",
},
{
name: 'fetch',
message: "Use `const { fetch } = require('internal/deps/undici/undici');` instead of the global.",
},
{
name: 'global',
message: 'Use `const { globalThis } = primordials;` instead of `global`.',
},
{
name: 'globalThis',
message: 'Use `const { globalThis } = primordials;` instead of the global.',
},
{
name: 'performance',
message: "Use `const { performance } = require('perf_hooks');` instead of the global.",
},
{
name: 'queueMicrotask',
message: "Use `const { queueMicrotask } = require('internal/process/task_queues');` instead of the global.",
},
{
name: 'setImmediate',
message: "Use `const { setImmediate } = require('timers');` instead of the global.",
},
{
name: 'setInterval',
message: "Use `const { setInterval } = require('timers');` instead of the global.",
},
{
name: 'setTimeout',
message: "Use `const { setTimeout } = require('timers');` instead of the global.",
},
{
name: 'structuredClone',
message: "Use `const { structuredClone } = internalBinding('messaging');` instead of the global.",
},
{
name: 'SubtleCrypto',
message: "Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.",
},
],
'no-restricted-modules': [
'error',
{
name: 'url',
message: 'Require `internal/url` instead of `url`.',
},
],
// Stylistic rules.
'@stylistic/js/no-mixed-operators': [
'error',
{
groups: [
['&&', '||'],
],
},
],
// Custom rules in tools/eslint-rules.
'node-core/alphabetize-errors': 'error',
'node-core/alphabetize-primordials': 'error',
'node-core/avoid-prototype-pollution': 'error',
'node-core/lowercase-name-for-primitive': 'error',
'node-core/non-ascii-character': 'error',
'node-core/no-array-destructuring': 'error',
'node-core/prefer-primordials': [
'error',
{ name: 'AggregateError' },
{ name: 'Array' },
{ name: 'ArrayBuffer' },
{ name: 'Atomics' },
{ name: 'BigInt' },
{ name: 'BigInt64Array' },
{ name: 'BigUint64Array' },
{ name: 'Boolean' },
{ name: 'DataView' },
{ name: 'Date' },
{ name: 'decodeURI' },
{ name: 'decodeURIComponent' },
{ name: 'encodeURI' },
{ name: 'encodeURIComponent' },
{ name: 'escape' },
{ name: 'eval' },
{
name: 'Error',
ignore: [
'prepareStackTrace',
'stackTraceLimit',
],
},
{ name: 'EvalError' },
{
name: 'FinalizationRegistry',
into: 'Safe',
},
{ name: 'Float32Array' },
{ name: 'Float64Array' },
{ name: 'Function' },
{ name: 'Int16Array' },
{ name: 'Int32Array' },
{ name: 'Int8Array' },
{
name: 'isFinite',
into: 'Number',
},
{
name: 'isNaN',
into: 'Number',
},
{ name: 'JSON' },
{
name: 'Map',
into: 'Safe',
},
{ name: 'Math' },
{ name: 'Number' },
{ name: 'Object' },
{
name: 'parseFloat',
into: 'Number',
},
{
name: 'parseInt',
into: 'Number',
},
{ name: 'Proxy' },
{ name: 'Promise' },
{ name: 'RangeError' },
{ name: 'ReferenceError' },
{ name: 'Reflect' },
{ name: 'RegExp' },
{
name: 'Set',
into: 'Safe',
},
{ name: 'String' },
{ name: 'Symbol' },
{ name: 'SyntaxError' },
{ name: 'TypeError' },
{ name: 'Uint16Array' },
{ name: 'Uint32Array' },
{ name: 'Uint8Array' },
{ name: 'Uint8ClampedArray' },
{ name: 'unescape' },
{ name: 'URIError' },
{
name: 'WeakMap',
into: 'Safe',
},
{
name: 'WeakRef',
into: 'Safe',
},
{
name: 'WeakSet',
into: 'Safe',
},
],
},
},
{
files: ['lib/internal/modules/**/*.js'],
rules: {
'curly': 'error',
},
},
{
files: ['lib/internal/per_context/primordials.js'],
rules: {
'node-core/alphabetize-primordials': [
'error',
{ enforceTopPosition: false },
],
},
},
{
files: ['lib/internal/test_runner/**/*.js'],
rules: {
'node-core/set-proto-to-null-in-object': 'error',
},
},
];

View file

@ -848,6 +848,7 @@ function guessHandleType(fd) {
class WeakReference {
#weak = null;
// eslint-disable-next-line no-unused-private-class-members
#strong = null;
#refCount = 0;
constructor(object) {

View file

@ -1,94 +0,0 @@
## Test-specific linter rules
env:
node: true
es6: true
rules:
multiline-comment-style: [error, separate-lines]
prefer-const: error
symbol-description: off
no-restricted-syntax:
# Config copied from .eslintrc.js
- error
- selector: CallExpression:matches([callee.name='deepStrictEqual'], [callee.property.name='deepStrictEqual']):matches([arguments.1.type='Literal']:not([arguments.1.regex]), [arguments.1.type='Identifier'][arguments.1.name='undefined'])
message: Use strictEqual instead of deepStrictEqual for literals or undefined.
- selector: CallExpression:matches([callee.name='notDeepStrictEqual'], [callee.property.name='notDeepStrictEqual']):matches([arguments.1.type='Literal']:not([arguments.1.regex]), [arguments.1.type='Identifier'][arguments.1.name='undefined'])
message: Use notStrictEqual instead of notDeepStrictEqual for literals or undefined.
- selector: CallExpression:matches([callee.name='deepStrictEqual'], [callee.property.name='deepStrictEqual'])[arguments.2.type='Literal']
message: Do not use a literal for the third argument of assert.deepStrictEqual()
- selector: CallExpression:matches([callee.name='doesNotThrow'], [callee.property.name='doesNotThrow'])
message: Do not use `assert.doesNotThrow()`. Write the code without the wrapper and add a comment instead.
- selector: CallExpression:matches([callee.name='doesNotReject'], [callee.property.name='doesNotReject'])
message: Do not use `assert.doesNotReject()`. Write the code without the wrapper and add a comment instead.
- selector: CallExpression:matches([callee.name='rejects'], [callee.property.name='rejects'])[arguments.length<2]
message: '`assert.rejects()` must be invoked with at least two arguments.'
- selector: CallExpression[callee.property.name='strictEqual'][arguments.2.type='Literal']
message: Do not use a literal for the third argument of assert.strictEqual()
- selector: CallExpression:matches([callee.name='throws'], [callee.property.name='throws'])[arguments.1.type='Literal']:not([arguments.1.regex])
message: Use an object as second argument of `assert.throws()`.
- selector: CallExpression:matches([callee.name='throws'], [callee.property.name='throws'])[arguments.length<2]
message: '`assert.throws()` must be invoked with at least two arguments.'
- selector: CallExpression[callee.name='setInterval'][arguments.length<2]
message: '`setInterval()` must be invoked with at least two arguments.'
- selector: ThrowStatement > CallExpression[callee.name=/Error$/]
message: Use `new` keyword when throwing an `Error`.
- selector: CallExpression:matches([callee.name='notDeepStrictEqual'], [callee.property.name='notDeepStrictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])
message: The first argument should be the `actual`, not the `expected` value.
- selector: CallExpression:matches([callee.name='notStrictEqual'], [callee.property.name='notStrictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])
message: The first argument should be the `actual`, not the `expected` value.
- selector: CallExpression:matches([callee.name='deepStrictEqual'], [callee.property.name='deepStrictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])
message: The first argument should be the `actual`, not the `expected` value.
- selector: CallExpression:matches([callee.name='strictEqual'], [callee.property.name='strictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])
message: The first argument should be the `actual`, not the `expected` value.
- selector: CallExpression[callee.name='isNaN']
message: Use Number.isNaN() instead of the global isNaN() function.
- selector: VariableDeclarator > CallExpression:matches([callee.name='debuglog'], [callee.property.name='debuglog']):not([arguments.0.value='test'])
message: Use 'test' as debuglog value in tests.
- selector: CallExpression:matches([callee.object.name="common"][callee.property.name=/^must(Not)?Call/],[callee.name="mustCall"],[callee.name="mustCallAtLeast"],[callee.name="mustNotCall"])>:first-child[type=/FunctionExpression$/][body.body.length=0]
message: Do not use an empty function, omit the parameter altogether.
- selector: ExpressionStatement>CallExpression:matches([callee.name='rejects'], [callee.object.name='assert'][callee.property.name='rejects'])
message: Calling `assert.rejects` without `await` or `.then(common.mustCall())` will not detect never-settling promises.
- selector: Identifier[name='webcrypto']
message: Use `globalThis.crypto`.
# Stylistic rules
'@stylistic/js/comma-dangle': [error, always-multiline]
# Custom rules in tools/eslint-rules
node-core/prefer-assert-iferror: error
node-core/prefer-assert-methods: error
node-core/prefer-common-mustnotcall: error
node-core/prefer-common-mustsucceed: error
node-core/crypto-check: error
node-core/eslint-check: error
node-core/async-iife-no-unused-result: error
node-core/inspector-check: error
## common module is mandatory in tests
node-core/required-modules:
- error
- common: common(/index\.(m)?js)?$
node-core/require-common-first: error
node-core/no-duplicate-requires: off
# Global scoped methods and vars
globals:
WebAssembly: false
overrides:
- files:
- es-module/*.js
- es-module/*.mjs
- parallel/*.js
- parallel/*.mjs
- sequential/*.js
- sequential/*.mjs
rules:
'@stylistic/js/comma-dangle': [error, {
arrays: always-multiline,
exports: always-multiline,
functions: only-multiline,
imports: always-multiline,
objects: only-multiline,
}]

View file

@ -1,3 +0,0 @@
rules:
node-core/required-modules: off
node-core/require-common-first: off

View file

@ -189,7 +189,7 @@ if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) {
}
initHandles[id] = {
resource,
stack: inspect(new Error()).substr(6),
stack: inspect(new Error()).slice(6),
};
},
before() { },

View file

@ -0,0 +1,165 @@
/* eslint-disable @stylistic/js/max-len */
import {
noRestrictedSyntaxCommonAll,
noRestrictedSyntaxCommonTest,
requireEslintTool,
} from '../tools/eslint.config_utils.mjs';
const globals = requireEslintTool('globals');
export default [
{
files: ['test/**/*.{js,mjs,cjs}'],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'multiline-comment-style': [
'error',
'separate-lines',
],
'prefer-const': 'error',
'symbol-description': 'off',
'no-restricted-syntax': [
'error',
...noRestrictedSyntaxCommonAll,
...noRestrictedSyntaxCommonTest,
{
selector: "CallExpression:matches([callee.name='deepStrictEqual'], [callee.property.name='deepStrictEqual']):matches([arguments.1.type='Literal']:not([arguments.1.regex]), [arguments.1.type='Identifier'][arguments.1.name='undefined'])",
message: 'Use strictEqual instead of deepStrictEqual for literals or undefined.',
},
{
selector: "CallExpression:matches([callee.name='notDeepStrictEqual'], [callee.property.name='notDeepStrictEqual']):matches([arguments.1.type='Literal']:not([arguments.1.regex]), [arguments.1.type='Identifier'][arguments.1.name='undefined'])",
message: 'Use notStrictEqual instead of notDeepStrictEqual for literals or undefined.',
},
{
selector: "CallExpression:matches([callee.name='deepStrictEqual'], [callee.property.name='deepStrictEqual'])[arguments.2.type='Literal']",
message: 'Do not use a literal for the third argument of assert.deepStrictEqual()',
},
{
selector: "CallExpression:matches([callee.name='doesNotThrow'], [callee.property.name='doesNotThrow'])",
message: 'Do not use `assert.doesNotThrow()`. Write the code without the wrapper and add a comment instead.',
},
{
selector: "CallExpression:matches([callee.name='doesNotReject'], [callee.property.name='doesNotReject'])",
message: 'Do not use `assert.doesNotReject()`. Write the code without the wrapper and add a comment instead.',
},
{
selector: "CallExpression:matches([callee.name='rejects'], [callee.property.name='rejects'])[arguments.length<2]",
message: '`assert.rejects()` must be invoked with at least two arguments.',
},
{
selector: "CallExpression[callee.property.name='strictEqual'][arguments.2.type='Literal']",
message: 'Do not use a literal for the third argument of assert.strictEqual()',
},
{
selector: "CallExpression:matches([callee.name='throws'], [callee.property.name='throws'])[arguments.1.type='Literal']:not([arguments.1.regex])",
message: 'Use an object as second argument of `assert.throws()`.',
},
{
selector: "CallExpression:matches([callee.name='throws'], [callee.property.name='throws'])[arguments.length<2]",
message: '`assert.throws()` must be invoked with at least two arguments.',
},
{
selector: "CallExpression:matches([callee.name='notDeepStrictEqual'], [callee.property.name='notDeepStrictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])",
message: 'The first argument should be the `actual`, not the `expected` value.',
},
{
selector: "CallExpression:matches([callee.name='notStrictEqual'], [callee.property.name='notStrictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])",
message: 'The first argument should be the `actual`, not the `expected` value.',
},
{
selector: "CallExpression:matches([callee.name='deepStrictEqual'], [callee.property.name='deepStrictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])",
message: 'The first argument should be the `actual`, not the `expected` value.',
},
{
selector: "CallExpression:matches([callee.name='strictEqual'], [callee.property.name='strictEqual'])[arguments.0.type='Literal']:not([arguments.1.type='Literal']):not([arguments.1.type='ObjectExpression']):not([arguments.1.type='ArrayExpression']):not([arguments.1.type='UnaryExpression'])",
message: 'The first argument should be the `actual`, not the `expected` value.',
},
{
selector: "CallExpression[callee.name='isNaN']",
message: 'Use `Number.isNaN()` instead of the global `isNaN()` function.',
},
{
selector: "VariableDeclarator > CallExpression:matches([callee.name='debuglog'], [callee.property.name='debuglog']):not([arguments.0.value='test'])",
message: "Use 'test' as debuglog value in tests.",
},
{
selector: 'CallExpression:matches([callee.object.name="common"][callee.property.name=/^must(Not)?Call/],[callee.name="mustCall"],[callee.name="mustCallAtLeast"],[callee.name="mustNotCall"])>:first-child[type=/FunctionExpression$/][body.body.length=0]',
message: 'Do not use an empty function, omit the parameter altogether.',
},
{
selector: "ExpressionStatement>CallExpression:matches([callee.name='rejects'], [callee.object.name='assert'][callee.property.name='rejects'])",
message: 'Calling `assert.rejects` without `await` or `.then(common.mustCall())` will not detect never-settling promises.',
},
],
// Stylistic rules.
'@stylistic/js/comma-dangle': [
'error',
'always-multiline',
],
// Custom rules in tools/eslint-rules.
'node-core/prefer-assert-iferror': 'error',
'node-core/prefer-assert-methods': 'error',
'node-core/prefer-common-mustnotcall': 'error',
'node-core/prefer-common-mustsucceed': 'error',
'node-core/crypto-check': 'error',
'node-core/eslint-check': 'error',
'node-core/async-iife-no-unused-result': 'error',
'node-core/inspector-check': 'error',
// `common` module is mandatory in tests.
'node-core/required-modules': [
'error',
{ common: 'common(/index\\.(m)?js)?$' },
],
'node-core/require-common-first': 'error',
'node-core/no-duplicate-requires': 'off',
},
},
{
files: [
'test/es-module/**/*.{js,mjs}',
'test/parallel/**/*.{js,mjs}',
'test/sequential/**/*.{js,mjs}',
],
rules: {
'@stylistic/js/comma-dangle': [
'error',
{
arrays: 'always-multiline',
exports: 'always-multiline',
functions: 'only-multiline',
imports: 'always-multiline',
objects: 'only-multiline',
},
],
},
},
{
files: [
'test/{common,wpt}/**/*.{js,mjs,cjs}',
'test/eslint.config_partial.mjs',
],
rules: {
'node-core/required-modules': 'off',
'node-core/require-common-first': 'off',
},
},
{
files: [
'test/es-module/test-esm-example-loader.js',
'test/es-module/test-esm-type-flag.js',
'test/es-module/test-esm-type-flag-alias.js',
'test/es-module/test-require-module-detect-entry-point.js',
'test/es-module/test-require-module-detect-entry-point-aou.js',
],
languageOptions: {
sourceType: 'module',
},
},
];

View file

@ -981,6 +981,7 @@ for (const input of inputs) {
{
assert.throws(() => {
// eslint-disable-next-line no-constant-binary-expression
assert.ok((() => Boolean('' === false))());
}, {
message: 'The expression evaluated to a falsy value:\n\n' +

View file

@ -9,10 +9,7 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/alphabetize-errors');
new RuleTester({
parserOptions: { ecmaVersion: 6 },
env: { es6: true }
}).run('alphabetize-errors', rule, {
new RuleTester().run('alphabetize-errors', rule, {
valid: [
{ code: `
E('AAA', 'foo');

View file

@ -10,10 +10,7 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/alphabetize-primordials');
new RuleTester({
parserOptions: { ecmaVersion: 6 },
env: { es6: true }
})
new RuleTester()
.run('alphabetize-primordials', rule, {
valid: [
'new Array()',

View file

@ -11,7 +11,7 @@ const rule = require('../../tools/eslint-rules/async-iife-no-unused-result');
const message = 'The result of an immediately-invoked async function needs ' +
'to be used (e.g. with `.then(common.mustCall())`)';
const tester = new RuleTester({ parserOptions: { ecmaVersion: 8 } });
const tester = new RuleTester();
tester.run('async-iife-no-unused-result', rule, {
valid: [
'(() => {})()',
@ -27,12 +27,10 @@ tester.run('async-iife-no-unused-result', rule, {
{
code: '(async () => {})()',
errors: [{ message }],
output: '(async () => {})()',
},
{
code: '(async function() {})()',
errors: [{ message }],
output: '(async function() {})()',
},
{
code: "const common = require('../common');(async () => {})()",

View file

@ -10,9 +10,7 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/avoid-prototype-pollution');
new RuleTester({
parserOptions: { ecmaVersion: 2022 },
})
new RuleTester()
.run('property-descriptor-no-prototype-pollution', rule, {
valid: [
'ObjectDefineProperties({}, {})',
@ -125,19 +123,46 @@ new RuleTester({
},
{
code: 'ObjectDefineProperty({}, "key", ObjectGetOwnPropertyDescriptor({}, "key"))',
errors: [{ message: /prototype pollution/ }],
errors: [{
message: /prototype pollution/,
suggestions: [{
desc: 'Wrap the property descriptor in a null-prototype object',
output: 'ObjectDefineProperty({}, "key", { __proto__: null,...ObjectGetOwnPropertyDescriptor({}, "key") })',
}],
}],
},
{
code: 'ReflectDefineProperty({}, "key", ObjectGetOwnPropertyDescriptor({}, "key"))',
errors: [{ message: /prototype pollution/ }],
errors: [{
message: /prototype pollution/,
suggestions: [{
desc: 'Wrap the property descriptor in a null-prototype object',
output:
'ReflectDefineProperty({}, "key", { __proto__: null,...ObjectGetOwnPropertyDescriptor({}, "key") })',
}],
}],
},
{
code: 'ObjectDefineProperty({}, "key", ReflectGetOwnPropertyDescriptor({}, "key"))',
errors: [{ message: /prototype pollution/ }],
errors: [{
message: /prototype pollution/,
suggestions: [{
desc: 'Wrap the property descriptor in a null-prototype object',
output:
'ObjectDefineProperty({}, "key", { __proto__: null,...ReflectGetOwnPropertyDescriptor({}, "key") })',
}],
}],
},
{
code: 'ReflectDefineProperty({}, "key", ReflectGetOwnPropertyDescriptor({}, "key"))',
errors: [{ message: /prototype pollution/ }],
errors: [{
message: /prototype pollution/,
suggestions: [{
desc: 'Wrap the property descriptor in a null-prototype object',
output:
'ReflectDefineProperty({}, "key", { __proto__: null,...ReflectGetOwnPropertyDescriptor({}, "key") })',
}],
}],
},
{
code: 'ObjectDefineProperty({}, "key", { __proto__: Object.prototype })',
@ -193,7 +218,13 @@ new RuleTester({
},
{
code: 'RegExpPrototypeTest(/some regex/, "some string")',
errors: [{ message: /looks up the "exec" property/ }],
errors: [{
message: /looks up the "exec" property/,
suggestions: [{
desc: 'Use RegexpPrototypeExec instead',
output: 'RegExpPrototypeExec(/some regex/, "some string") !== null',
}],
}],
},
{
code: 'RegExpPrototypeSymbolMatch(/some regex/, "some string")',

View file

@ -10,7 +10,11 @@ common.skipIfEslintMissing();
const { RuleTester } = require('../../tools/node_modules/eslint');
const rule = require('../../tools/eslint-rules/no-duplicate-requires');
new RuleTester().run('no-duplicate-requires', rule, {
new RuleTester({
languageOptions: {
sourceType: 'script',
},
}).run('no-duplicate-requires', rule, {
valid: [
{
code: 'require("a"); require("b"); (function() { require("a"); });',

View file

@ -15,10 +15,7 @@ const USE_OBJ_DESTRUCTURING =
const USE_ARRAY_METHODS =
'Use primordials.ArrayPrototypeSlice to avoid unsafe array iteration.';
new RuleTester({
parserOptions: { ecmaVersion: 2021 },
env: { es6: true }
})
new RuleTester()
.run('no-array-destructuring', rule, {
valid: [
'const first = [1, 2, 3][0];',

View file

@ -15,9 +15,7 @@ const msg1 = 'Please use common.mustSucceed instead of ' +
const msg2 = 'Please use common.mustSucceed instead of ' +
'common.mustCall with assert.ifError.';
new RuleTester({
parserOptions: { ecmaVersion: 2015 }
}).run('prefer-common-mustsucceed', rule, {
new RuleTester().run('prefer-common-mustsucceed', rule, {
valid: [
'foo((err) => assert.ifError(err))',
'foo(function(err) { assert.ifError(err) })',

View file

@ -11,8 +11,9 @@ const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/prefer-primordials');
new RuleTester({
parserOptions: { ecmaVersion: 6 },
env: { es6: true }
languageOptions: {
sourceType: 'script',
},
})
.run('prefer-primordials', rule, {
valid: [

View file

@ -10,9 +10,7 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/prefer-proto');
new RuleTester({
parserOptions: { ecmaVersion: 2022 }
}).run('prefer-common-mustsucceed', rule, {
new RuleTester().run('prefer-common-mustsucceed', rule, {
valid: [
'({ __proto__: null })',
'const x = { __proto__: null };',

View file

@ -12,7 +12,7 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/prefer-util-format-errors');
new RuleTester({ parserOptions: { ecmaVersion: 6 } })
new RuleTester()
.run('prefer-util-format-errors', rule, {
valid: [
'E(\'ABC\', \'abc\');',

View file

@ -10,7 +10,11 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/require-common-first');
new RuleTester().run('require-common-first', rule, {
new RuleTester({
languageOptions: {
sourceType: 'script',
},
}).run('require-common-first', rule, {
valid: [
{
code: 'require("common")\n' +

View file

@ -10,7 +10,11 @@ common.skipIfEslintMissing();
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/required-modules');
new RuleTester().run('required-modules', rule, {
new RuleTester({
languageOptions: {
sourceType: 'script',
},
}).run('required-modules', rule, {
valid: [
{
code: 'require("common")',

View file

@ -559,6 +559,7 @@ test('mocks a constructor', (t) => {
}
class MockClazz {
// eslint-disable-next-line no-unused-private-class-members
#privateValue;
constructor(z) {

View file

@ -1,3 +0,0 @@
rules:
node-core/required-modules: off
node-core/require-common-first: off

View file

@ -1,14 +0,0 @@
env:
node: true
es6: true
rules:
camelcase:
- error
- properties: never
ignoreDestructuring: true
allow: [child_process]
no-unused-vars:
- error
- args: after-used
prefer-arrow-callback: error

View file

@ -43,8 +43,10 @@ rm -rf ../node_modules/eslint
"$NODE" "$NPM" uninstall \
--install-links=false \
--ignore-scripts \
eslint-formatter-tap \
eslint-plugin-jsdoc \
eslint-plugin-markdown \
globals \
@babel/core \
@babel/eslint-parser \
@babel/plugin-syntax-import-attributes \
@ -59,8 +61,10 @@ rm -rf ../node_modules/eslint
--no-save \
--omit=dev \
--omit=peer \
eslint-formatter-tap \
eslint-plugin-jsdoc \
eslint-plugin-markdown \
globals \
@babel/core \
@babel/eslint-parser \
@babel/plugin-syntax-import-attributes \

View file

@ -148,9 +148,9 @@ module.exports = {
suggest: [{
desc: 'Use RegexpPrototypeExec instead',
fix(fixer) {
const testRange = { ...node.range };
testRange.start = testRange.start + 'RegexpPrototype'.length;
testRange.end = testRange.start + 'Test'.length;
const testRange = [ ...node.range ];
testRange[0] = testRange[0] + 'RegexpPrototype'.length;
testRange[1] = testRange[0] + 'Test'.length;
return [
fixer.replaceTextRange(testRange, 'Exec'),
fixer.insertTextAfter(node, ' !== null'),

View file

@ -27,7 +27,7 @@ function isTopLevel(node) {
module.exports = {
create(context) {
if (context.parserOptions.sourceType === 'module') {
if (context.languageOptions.sourceType === 'module') {
return {};
}

View file

@ -13,7 +13,7 @@ const { isRequireCall, isString } = require('./rules-utils.js');
module.exports = {
create(context) {
const requiredModule = 'common';
const isESM = context.parserOptions.sourceType === 'module';
const isESM = context.languageOptions.sourceType === 'module';
const foundModules = [];
/**

View file

@ -23,7 +23,7 @@ module.exports = {
const requiredModules = options ? Object.keys(options).map((x) => {
return [ x, new RegExp(options[x]) ];
}) : [];
const isESM = context.parserOptions.sourceType === 'module';
const isESM = context.languageOptions.sourceType === 'module';
const foundModules = [];

View file

@ -0,0 +1,29 @@
import { requireEslintTool } from './eslint.config_utils.mjs';
const globals = requireEslintTool('globals');
export default [
{
files: ['tools/**/*.{js,mjs,cjs}'],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'camelcase': [
'error',
{
properties: 'never',
ignoreDestructuring: true,
allow: ['child_process'],
},
],
'no-unused-vars': [
'error',
{ args: 'after-used' },
],
'prefer-arrow-callback': 'error',
},
},
];

View file

@ -0,0 +1,35 @@
import { createRequire } from 'node:module';
export const requireEslintTool = createRequire(new URL('./node_modules/eslint/', import.meta.url));
export const resolveEslintTool = (request) => requireEslintTool.resolve(request);
export const noRestrictedSyntaxCommonAll = [
{
selector: "CallExpression[callee.name='setInterval'][arguments.length<2]",
message: '`setInterval()` must be invoked with at least two arguments.',
},
{
selector: 'ThrowStatement > CallExpression[callee.name=/Error$/]',
message: 'Use `new` keyword when throwing an `Error`.',
},
{
selector: "CallExpression[callee.property.name='substr']",
message: 'Use String.prototype.slice() or String.prototype.substring() instead of String.prototype.substr()',
},
];
export const noRestrictedSyntaxCommonLib = [
{
selector: "CallExpression[callee.name='setTimeout'][arguments.length<2]",
message: '`setTimeout()` must be invoked with at least two arguments.',
},
];
export const noRestrictedSyntaxCommonTest = [
{
// TODO(@panva): move this to no-restricted-properties
// when https://github.com/eslint/eslint/issues/16412 is fixed.
selector: "Identifier[name='webcrypto']",
message: 'Use `globalThis.crypto`.',
},
];

View file

@ -928,6 +928,13 @@ int Main(int argc, char* argv[]) {
auto mjs_it = file_map.find(".mjs");
assert(js_it != file_map.end() && mjs_it != file_map.end());
auto it = std::find(mjs_it->second.begin(),
mjs_it->second.end(),
"lib/eslint.config_partial.mjs");
if (it != mjs_it->second.end()) {
mjs_it->second.erase(it);
}
std::sort(js_it->second.begin(), js_it->second.end());
std::sort(mjs_it->second.begin(), mjs_it->second.end());

View file

@ -64,7 +64,7 @@ function readStdin() {
function getErrorMessage(error) {
// Lazy loading because this is used only if an error happened.
const util = require("util");
const util = require("node:util");
// Foolproof -- third-party module might throw non-object.
if (typeof error !== "object" || error === null) {
@ -140,16 +140,17 @@ ${getErrorMessage(error)}`;
if (process.argv.includes("--init")) {
// `eslint --init` has been moved to `@eslint/create-config`
console.warn("You can also run this command directly using 'npm init @eslint/config'.");
console.warn("You can also run this command directly using 'npm init @eslint/config@latest'.");
const spawn = require("cross-spawn");
spawn.sync("npm", ["init", "@eslint/config"], { encoding: "utf8", stdio: "inherit" });
spawn.sync("npm", ["init", "@eslint/config@latest"], { encoding: "utf8", stdio: "inherit" });
return;
}
// Otherwise, call the CLI.
const exitCode = await require("../lib/cli").execute(
const cli = require("../lib/cli");
const exitCode = await cli.execute(
process.argv,
process.argv.includes("--stdin") ? await readStdin() : null,
true

View file

@ -1,93 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview Defines a schema for configs.
* @author Sylvan Mably
*/
"use strict";
const baseConfigProperties = {
$schema: { type: "string" },
env: { type: "object" },
extends: { $ref: "#/definitions/stringOrStrings" },
globals: { type: "object" },
overrides: {
type: "array",
items: { $ref: "#/definitions/overrideConfig" },
additionalItems: false
},
parser: { type: ["string", "null"] },
parserOptions: { type: "object" },
plugins: { type: "array" },
processor: { type: "string" },
rules: { type: "object" },
settings: { type: "object" },
noInlineConfig: { type: "boolean" },
reportUnusedDisableDirectives: { type: "boolean" },
ecmaFeatures: { type: "object" } // deprecated; logs a warning when used
};
const configSchema = {
definitions: {
stringOrStrings: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
additionalItems: false
}
]
},
stringOrStringsRequired: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
additionalItems: false,
minItems: 1
}
]
},
// Config at top-level.
objectConfig: {
type: "object",
properties: {
root: { type: "boolean" },
ignorePatterns: { $ref: "#/definitions/stringOrStrings" },
...baseConfigProperties
},
additionalProperties: false
},
// Config in `overrides`.
overrideConfig: {
type: "object",
properties: {
excludedFiles: { $ref: "#/definitions/stringOrStrings" },
files: { $ref: "#/definitions/stringOrStringsRequired" },
...baseConfigProperties
},
required: ["files"],
additionalProperties: false
}
},
$ref: "#/definitions/objectConfig"
};
module.exports = configSchema;

16
tools/node_modules/eslint/conf/ecma-version.js generated vendored Normal file
View file

@ -0,0 +1,16 @@
/**
* @fileoverview Configuration related to ECMAScript versions
* @author Milos Djermanovic
*/
"use strict";
/**
* The latest ECMAScript version supported by ESLint.
* @type {number} year-based ECMAScript version
*/
const LATEST_ECMA_VERSION = 2024;
module.exports = {
LATEST_ECMA_VERSION
};

View file

@ -70,6 +70,7 @@ const es2015 = {
Int16Array: false,
Int32Array: false,
Int8Array: false,
Intl: false,
Map: false,
Promise: false,
Proxy: false,

View file

@ -23,6 +23,8 @@
{ "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] },
{ "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] },
{ "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] },
{ "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }
{ "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] },
{ "removed": "valid-jsdoc", "replacedBy": [] },
{ "removed": "require-jsdoc", "replacedBy": [] }
]
}

18
tools/node_modules/eslint/lib/api.js generated vendored
View file

@ -9,8 +9,8 @@
// Requirements
//-----------------------------------------------------------------------------
const { ESLint, FlatESLint } = require("./eslint");
const { shouldUseFlatConfig } = require("./eslint/flat-eslint");
const { ESLint, shouldUseFlatConfig } = require("./eslint/eslint");
const { LegacyESLint } = require("./eslint/legacy-eslint");
const { Linter } = require("./linter");
const { RuleTester } = require("./rule-tester");
const { SourceCode } = require("./source-code");
@ -23,22 +23,18 @@ const { SourceCode } = require("./source-code");
* Loads the correct ESLint constructor given the options.
* @param {Object} [options] The options object
* @param {boolean} [options.useFlatConfig] Whether or not to use a flat config
* @param {string} [options.cwd] The current working directory
* @returns {Promise<ESLint|LegacyESLint>} The ESLint constructor
*/
async function loadESLint({ useFlatConfig, cwd = process.cwd() } = {}) {
async function loadESLint({ useFlatConfig } = {}) {
/*
* Note: The v9.x version of this function doesn't have a cwd option
* because it's not used. It's only used in the v8.x version of this
* function.
* Note: The v8.x version of this function also accepted a `cwd` option, but
* it is not used in this implementation so we silently ignore it.
*/
const shouldESLintUseFlatConfig = typeof useFlatConfig === "boolean"
? useFlatConfig
: await shouldUseFlatConfig({ cwd });
const shouldESLintUseFlatConfig = useFlatConfig ?? (await shouldUseFlatConfig());
return shouldESLintUseFlatConfig ? FlatESLint : ESLint;
return shouldESLintUseFlatConfig ? ESLint : LegacyESLint;
}
//-----------------------------------------------------------------------------

View file

@ -15,8 +15,8 @@
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs");
const path = require("path");
const fs = require("node:fs");
const path = require("node:path");
const defaultOptions = require("../../conf/default-cli-options");
const pkg = require("../../package.json");
@ -41,6 +41,17 @@ const hash = require("./hash");
const LintResultCache = require("./lint-result-cache");
const debug = require("debug")("eslint:cli-engine");
const removedFormatters = new Set([
"checkstyle",
"codeframe",
"compact",
"jslint-xml",
"junit",
"table",
"tap",
"unix",
"visualstudio"
]);
const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]);
//------------------------------------------------------------------------------
@ -639,7 +650,7 @@ class CLIEngine {
});
const lintResultCache =
options.cache ? new LintResultCache(cacheFilePath, options.cacheStrategy) : null;
const linter = new Linter({ cwd: options.cwd });
const linter = new Linter({ cwd: options.cwd, configType: "eslintrc" });
/** @type {ConfigArray[]} */
const lastConfigArrays = [configArrayFactory.getConfigArrayForFile()];
@ -721,7 +732,7 @@ class CLIEngine {
* @returns {void}
*/
static outputFixes(report) {
report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => {
report.results.filter(result => Object.hasOwn(result, "output")).forEach(result => {
fs.writeFileSync(result.filePath, result.output);
});
}
@ -1047,7 +1058,7 @@ class CLIEngine {
try {
return require(formatterPath);
} catch (ex) {
if (format === "table" || format === "codeframe") {
if (removedFormatters.has(format)) {
ex.message = `The ${format} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${format}\``;
} else {
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;

View file

@ -34,8 +34,8 @@
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs");
const path = require("path");
const fs = require("node:fs");
const path = require("node:path");
const getGlobParent = require("glob-parent");
const isGlob = require("is-glob");
const escapeRegExp = require("escape-string-regexp");

View file

@ -1,60 +0,0 @@
/**
* @fileoverview CheckStyle XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<checkstyle version=\"4.3\">";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${xmlEscape(result.filePath)}">`;
messages.forEach(message => {
output += [
`<error line="${xmlEscape(message.line || 0)}"`,
`column="${xmlEscape(message.column || 0)}"`,
`severity="${xmlEscape(getMessageType(message))}"`,
`message="${xmlEscape(message.message)}${message.ruleId ? ` (${message.ruleId})` : ""}"`,
`source="${message.ruleId ? xmlEscape(`eslint.rules.${message.ruleId}`) : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</checkstyle>";
return output;
};

View file

@ -1,60 +0,0 @@
/**
* @fileoverview Compact reporter
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}: `;
output += `line ${message.line || 0}`;
output += `, col ${message.column || 0}`;
output += `, ${getMessageType(message)}`;
output += ` - ${message.message}`;
output += message.ruleId ? ` (${message.ruleId})` : "";
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};

View file

@ -1,20 +1,8 @@
[
{
"name": "checkstyle",
"description": "Outputs results to the [Checkstyle](https://checkstyle.sourceforge.io/) format."
},
{
"name": "compact",
"description": "Human-readable output format. Mimics the default output of JSHint."
},
{
"name": "html",
"description": "Outputs results to HTML. The `html` formatter is useful for visual presentation in the browser."
},
{
"name": "jslint-xml",
"description": "Outputs results to format compatible with the [JSLint Jenkins plugin](https://plugins.jenkins.io/jslint/)."
},
{
"name": "json-with-metadata",
"description": "Outputs JSON-serialized results. The `json-with-metadata` provides the same linting results as the [`json`](#json) formatter with additional metadata about the rules applied. The linting results are included in the `results` property and the rules metadata is included in the `metadata` property.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint."
@ -23,24 +11,8 @@
"name": "json",
"description": "Outputs JSON-serialized results. The `json` formatter is useful when you want to programmatically work with the CLI's linting results.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint."
},
{
"name": "junit",
"description": "Outputs results to format compatible with the [JUnit Jenkins plugin](https://plugins.jenkins.io/junit/)."
},
{
"name": "stylish",
"description": "Human-readable output format. This is the default formatter."
},
{
"name": "tap",
"description": "Outputs results to the [Test Anything Protocol (TAP)](https://testanything.org/) specification format."
},
{
"name": "unix",
"description": "Outputs results to a format similar to many commands in UNIX-like systems. Parsable with tools such as [grep](https://www.gnu.org/software/grep/manual/grep.html), [sed](https://www.gnu.org/software/sed/manual/sed.html), and [awk](https://www.gnu.org/software/gawk/manual/gawk.html)."
},
{
"name": "visualstudio",
"description": "Outputs results to format compatible with the integrated terminal of the [Visual Studio](https://visualstudio.microsoft.com/) IDE. When using Visual Studio, you can click on the linting results in the integrated terminal to go to the issue in the source code."
}
]
]

View file

@ -1,41 +0,0 @@
/**
* @fileoverview JSLint XML reporter
* @author Ian Christian Myers
*/
"use strict";
const xmlEscape = require("../xml-escape");
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
output += "<jslint>";
results.forEach(result => {
const messages = result.messages;
output += `<file name="${result.filePath}">`;
messages.forEach(message => {
output += [
`<issue line="${message.line}"`,
`char="${message.column}"`,
`evidence="${xmlEscape(message.source || "")}"`,
`reason="${xmlEscape(message.message || "")}${message.ruleId ? ` (${message.ruleId})` : ""}" />`
].join(" ");
});
output += "</file>";
});
output += "</jslint>";
return output;
};

View file

@ -1,82 +0,0 @@
/**
* @fileoverview jUnit Reporter
* @author Jamund Ferguson
*/
"use strict";
const xmlEscape = require("../xml-escape");
const path = require("path");
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
/**
* Returns a full file path without extension
* @param {string} filePath input file path
* @returns {string} file path without extension
* @private
*/
function pathWithoutExt(filePath) {
return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)));
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "";
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
output += "<testsuites>\n";
results.forEach(result => {
const messages = result.messages;
const classname = pathWithoutExt(result.filePath);
if (messages.length > 0) {
output += `<testsuite package="org.eslint" time="0" tests="${messages.length}" errors="${messages.length}" name="${result.filePath}">\n`;
messages.forEach(message => {
const type = message.fatal ? "error" : "failure";
output += `<testcase time="0" name="org.eslint.${message.ruleId || "unknown"}" classname="${classname}">`;
output += `<${type} message="${xmlEscape(message.message || "")}">`;
output += "<![CDATA[";
output += `line ${message.line || 0}, col `;
output += `${message.column || 0}, ${getMessageType(message)}`;
output += ` - ${xmlEscape(message.message || "")}`;
output += (message.ruleId ? ` (${message.ruleId})` : "");
output += "]]>";
output += `</${type}>`;
output += "</testcase>\n";
});
output += "</testsuite>\n";
} else {
output += `<testsuite package="org.eslint" time="0" tests="1" errors="0" name="${result.filePath}">\n`;
output += `<testcase time="0" name="${result.filePath}" classname="${classname}" />\n`;
output += "</testsuite>\n";
}
});
output += "</testsuites>\n";
return output;
};

View file

@ -1,58 +0,0 @@
/**
* @fileoverview unix-style formatter.
* @author oshi-shinobu
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns a canonical error level string based upon the error message passed in.
* @param {Object} message Individual error message provided by eslint
* @returns {string} Error level string
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "Error";
}
return "Warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += `${result.filePath}:`;
output += `${message.line || 0}:`;
output += `${message.column || 0}:`;
output += ` ${message.message} `;
output += `[${getMessageType(message)}${message.ruleId ? `/${message.ruleId}` : ""}]`;
output += "\n";
});
});
if (total > 0) {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};

View file

@ -1,63 +0,0 @@
/**
* @fileoverview Visual Studio compatible formatter
* @author Ronald Pijnacker
*/
"use strict";
//------------------------------------------------------------------------------
// Helper Functions
//------------------------------------------------------------------------------
/**
* Returns the severity of warning or error
* @param {Object} message message object to examine
* @returns {string} severity level
* @private
*/
function getMessageType(message) {
if (message.fatal || message.severity === 2) {
return "error";
}
return "warning";
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = function(results) {
let output = "",
total = 0;
results.forEach(result => {
const messages = result.messages;
total += messages.length;
messages.forEach(message => {
output += result.filePath;
output += `(${message.line || 0}`;
output += message.column ? `,${message.column}` : "";
output += `): ${getMessageType(message)}`;
output += message.ruleId ? ` ${message.ruleId}` : "";
output += ` : ${message.message}`;
output += "\n";
});
});
if (total === 0) {
output += "no problems";
} else {
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
}
return output;
};

View file

@ -8,8 +8,8 @@
// Requirements
//-----------------------------------------------------------------------------
const assert = require("assert");
const fs = require("fs");
const assert = require("node:assert");
const fs = require("node:fs");
const fileEntryCache = require("file-entry-cache");
const stringify = require("json-stable-stringify-without-jsonify");
const pkg = require("../../package.json");
@ -164,7 +164,7 @@ class LintResultCache {
* @returns {void}
*/
setCachedLintResults(filePath, config, result) {
if (result && Object.prototype.hasOwnProperty.call(result, "output")) {
if (result && Object.hasOwn(result, "output")) {
return;
}
@ -181,7 +181,7 @@ class LintResultCache {
* In `getCachedLintResults`, if source is explicitly null, we will
* read the file from the filesystem to set the value again.
*/
if (Object.prototype.hasOwnProperty.call(resultToSerialize, "source")) {
if (Object.hasOwn(resultToSerialize, "source")) {
resultToSerialize.source = null;
}

View file

@ -9,8 +9,8 @@
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path");
const fs = require("node:fs"),
path = require("node:path");
const rulesDirCache = {};

View file

@ -1,34 +0,0 @@
/**
* @fileoverview XML character escaper
* @author George Chung
*/
"use strict";
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Returns the escaped value for a character
* @param {string} s string to examine
* @returns {string} severity level
* @private
*/
module.exports = function(s) {
return (`${s}`).replace(/[<>&"'\x00-\x1F\x7F\u0080-\uFFFF]/gu, c => { // eslint-disable-line no-control-regex -- Converting controls to entities
switch (c) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "\"":
return "&quot;";
case "'":
return "&apos;";
default:
return `&#${c.charCodeAt(0)};`;
}
});
};

159
tools/node_modules/eslint/lib/cli.js generated vendored
View file

@ -15,11 +15,11 @@
// Requirements
//------------------------------------------------------------------------------
const fs = require("fs"),
path = require("path"),
{ promisify } = require("util"),
{ ESLint } = require("./eslint"),
{ FlatESLint, shouldUseFlatConfig } = require("./eslint/flat-eslint"),
const fs = require("node:fs"),
path = require("node:path"),
{ promisify } = require("node:util"),
{ LegacyESLint } = require("./eslint"),
{ ESLint, shouldUseFlatConfig, locateConfigFileToUse } = require("./eslint/eslint"),
createCLIOptions = require("./options"),
log = require("./shared/logging"),
RuntimeInfo = require("./shared/runtime-info"),
@ -37,6 +37,7 @@ const debug = require("debug")("eslint:cli");
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
/** @typedef {import("./shared/types").Plugin} Plugin */
/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
//------------------------------------------------------------------------------
@ -47,6 +48,32 @@ const mkdir = promisify(fs.mkdir);
const stat = promisify(fs.stat);
const writeFile = promisify(fs.writeFile);
/**
* Loads plugins with the specified names.
* @param {{ "import": (name: string) => Promise<any> }} importer An object with an `import` method called once for each plugin.
* @param {string[]} pluginNames The names of the plugins to be loaded, with or without the "eslint-plugin-" prefix.
* @returns {Promise<Record<string, Plugin>>} A mapping of plugin short names to implementations.
*/
async function loadPlugins(importer, pluginNames) {
const plugins = {};
await Promise.all(pluginNames.map(async pluginName => {
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
const module = await importer.import(longName);
if (!("default" in module)) {
throw new Error(`"${longName}" cannot be used with the \`--plugin\` option because its default module does not provide a \`default\` export`);
}
const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
plugins[shortName] = module.default;
}));
return plugins;
}
/**
* Predicate function for whether or not to apply fixes in quiet mode.
* If a message is a warning, do not apply a fix.
@ -58,6 +85,16 @@ function quietFixPredicate(message) {
return message.severity === 2;
}
/**
* Predicate function for whether or not to run a rule in quiet mode.
* If a rule is set to warning, do not run it.
* @param {{ ruleId: string; severity: number; }} rule The rule id and severity.
* @returns {boolean} True if the lint rule should run, false otherwise.
*/
function quietRuleFilter(rule) {
return rule.severity === 2;
}
/**
* Translates the CLI options into the options expected by the ESLint constructor.
* @param {ParsedCLIOptions} cliOptions The CLI options to translate.
@ -94,7 +131,10 @@ async function translateOptions({
resolvePluginsRelativeTo,
rule,
rulesdir,
warnIgnored
stats,
warnIgnored,
passOnNoPatterns,
maxWarnings
}, configType) {
let overrideConfig, overrideConfigFile;
@ -140,17 +180,7 @@ async function translateOptions({
}
if (plugin) {
const plugins = {};
for (const pluginName of plugin) {
const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
plugins[shortName] = await importer.import(longName);
}
overrideConfig[0].plugins = plugins;
overrideConfig[0].plugins = await loadPlugins(importer, plugin);
}
} else {
@ -187,12 +217,20 @@ async function translateOptions({
fixTypes: fixType,
ignore,
overrideConfig,
overrideConfigFile
overrideConfigFile,
passOnNoPatterns
};
if (configType === "flat") {
options.ignorePatterns = ignorePattern;
options.stats = stats;
options.warnIgnored = warnIgnored;
/*
* For performance reasons rules not marked as 'error' are filtered out in quiet mode. As maxWarnings
* requires rules set to 'warn' to be run, we only filter out 'warn' rules if maxWarnings is not specified.
*/
options.ruleFilter = quiet && maxWarnings === -1 ? quietRuleFilter : () => true;
} else {
options.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
options.rulePaths = rulesdir;
@ -266,25 +304,23 @@ async function printResults(engine, results, format, outputFile, resultsMeta) {
const output = await formatter.format(results, resultsMeta);
if (output) {
if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile);
if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile);
if (await isDirectory(filePath)) {
log.error("Cannot write to output file path, it is a directory: %s", outputFile);
return false;
}
try {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, output);
} catch (ex) {
log.error("There was a problem writing the output file:\n%s", ex);
return false;
}
} else {
log.info(output);
if (await isDirectory(filePath)) {
log.error("Cannot write to output file path, it is a directory: %s", outputFile);
return false;
}
try {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, output);
} catch (ex) {
log.error("There was a problem writing the output file:\n%s", ex);
return false;
}
} else if (output) {
log.info(output);
}
return true;
@ -300,14 +336,35 @@ async function printResults(engine, results, format, outputFile, resultsMeta) {
*/
const cli = {
/**
* Calculates the command string for the --inspect-config operation.
* @param {string} configFile The path to the config file to inspect.
* @returns {Promise<string>} The command string to execute.
*/
async calculateInspectConfigFlags(configFile) {
// find the config file
const {
configFilePath,
basePath,
error
} = await locateConfigFileToUse({ cwd: process.cwd(), configFile });
if (error) {
throw error;
}
return ["--config", configFilePath, "--basePath", basePath];
},
/**
* Executes the CLI based on an array of arguments that is passed in.
* @param {string|Array|Object} args The arguments to process.
* @param {string} [text] The text to lint (used for TTY).
* @param {boolean} [allowFlatConfig] Whether or not to allow flat config.
* @param {boolean} [allowFlatConfig=true] Whether or not to allow flat config.
* @returns {Promise<number>} The exit code for the operation.
*/
async execute(args, text, allowFlatConfig) {
async execute(args, text, allowFlatConfig = true) {
if (Array.isArray(args)) {
debug("CLI args: %o", args.slice(2));
}
@ -323,6 +380,10 @@ const cli = {
debug("Using flat config?", usingFlatConfig);
if (allowFlatConfig && !usingFlatConfig) {
process.emitWarning("You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details.", "ESLintRCWarning");
}
const CLIOptions = createCLIOptions(usingFlatConfig);
/** @type {ParsedCLIOptions} */
@ -376,8 +437,8 @@ const cli = {
}
const engine = usingFlatConfig
? new FlatESLint(await translateOptions(options, "flat"))
: new ESLint(await translateOptions(options));
? new ESLint(await translateOptions(options, "flat"))
: new LegacyESLint(await translateOptions(options));
const fileConfig =
await engine.calculateConfigForFile(options.printConfig);
@ -385,6 +446,24 @@ const cli = {
return 0;
}
if (options.inspectConfig) {
log.info("You can also run this command directly using 'npx @eslint/config-inspector' in the same directory as your configuration file.");
try {
const flatOptions = await translateOptions(options, "flat");
const spawn = require("cross-spawn");
const flags = await cli.calculateInspectConfigFlags(flatOptions.overrideConfigFile);
spawn.sync("npx", ["@eslint/config-inspector", ...flags], { encoding: "utf8", stdio: "inherit" });
} catch (error) {
log.error(error);
return 2;
}
return 0;
}
debug(`Running on ${useStdin ? "text" : "files"}`);
if (options.fix && options.fixDryRun) {
@ -405,7 +484,7 @@ const cli = {
return 2;
}
const ActiveESLint = usingFlatConfig ? FlatESLint : ESLint;
const ActiveESLint = usingFlatConfig ? ESLint : LegacyESLint;
const engine = new ActiveESLint(await translateOptions(options, usingFlatConfig ? "flat" : "eslintrc"));
let results;

View file

@ -42,6 +42,9 @@ exports.defaultConfig = [
ecmaVersion: "latest",
parser: require("espree"),
parserOptions: {}
},
linterOptions: {
reportUnusedDisableDirectives: 1
}
},

View file

@ -13,12 +13,16 @@ const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array"
const { flatConfigSchema } = require("./flat-config-schema");
const { RuleValidator } = require("./rule-validator");
const { defaultConfig } = require("./default-config");
const jsPlugin = require("@eslint/js");
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Fields that are considered metadata and not part of the config object.
*/
const META_FIELDS = new Set(["name"]);
const ruleValidator = new RuleValidator();
/**
@ -75,7 +79,53 @@ function getObjectId(object) {
return name;
}
/**
* Wraps a config error with details about where the error occurred.
* @param {Error} error The original error.
* @param {number} originalLength The original length of the config array.
* @param {number} baseLength The length of the base config.
* @returns {TypeError} The new error with details.
*/
function wrapConfigErrorWithDetails(error, originalLength, baseLength) {
let location = "user-defined";
let configIndex = error.index;
/*
* A config array is set up in this order:
* 1. Base config
* 2. Original configs
* 3. User-defined configs
* 4. CLI-defined configs
*
* So we need to adjust the index to account for the base config.
*
* - If the index is less than the base length, it's in the base config
* (as specified by `baseConfig` argument to `FlatConfigArray` constructor).
* - If the index is greater than the base length but less than the original
* length + base length, it's in the original config. The original config
* is passed to the `FlatConfigArray` constructor as the first argument.
* - Otherwise, it's in the user-defined config, which is loaded from the
* config file and merged with any command-line options.
*/
if (error.index < baseLength) {
location = "base";
} else if (error.index < originalLength + baseLength) {
location = "original";
configIndex = error.index - baseLength;
} else {
configIndex = error.index - originalLength - baseLength;
}
return new TypeError(
`${error.message.slice(0, -1)} at ${location} index ${configIndex}.`,
{ cause: error }
);
}
const originalBaseConfig = Symbol("originalBaseConfig");
const originalLength = Symbol("originalLength");
const baseLength = Symbol("baseLength");
//-----------------------------------------------------------------------------
// Exports
@ -102,12 +152,24 @@ class FlatConfigArray extends ConfigArray {
schema: flatConfigSchema
});
/**
* The original length of the array before any modifications.
* @type {number}
*/
this[originalLength] = this.length;
if (baseConfig[Symbol.iterator]) {
this.unshift(...baseConfig);
} else {
this.unshift(baseConfig);
}
/**
* The length of the array after applying the base config.
* @type {number}
*/
this[baseLength] = this.length - this[originalLength];
/**
* The base config used to build the config array.
* @type {Array<FlatConfig>}
@ -125,6 +187,49 @@ class FlatConfigArray extends ConfigArray {
Object.defineProperty(this, "shouldIgnore", { writable: false });
}
/**
* Normalizes the array by calling the superclass method and catching/rethrowing
* any ConfigError exceptions with additional details.
* @param {any} [context] The context to use to normalize the array.
* @returns {Promise<FlatConfigArray>} A promise that resolves when the array is normalized.
*/
normalize(context) {
return super.normalize(context)
.catch(error => {
if (error.name === "ConfigError") {
throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
}
throw error;
});
}
/**
* Normalizes the array by calling the superclass method and catching/rethrowing
* any ConfigError exceptions with additional details.
* @param {any} [context] The context to use to normalize the array.
* @returns {FlatConfigArray} The current instance.
* @throws {TypeError} If the config is invalid.
*/
normalizeSync(context) {
try {
return super.normalizeSync(context);
} catch (error) {
if (error.name === "ConfigError") {
throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
}
throw error;
}
}
/* eslint-disable class-methods-use-this -- Desired as instance method */
/**
* Replaces a config with another config to allow us to put strings
@ -134,36 +239,17 @@ class FlatConfigArray extends ConfigArray {
* @returns {Object} The preprocessed config.
*/
[ConfigArraySymbol.preprocessConfig](config) {
if (config === "eslint:recommended") {
// if we are in a Node.js environment warn the user
if (typeof process !== "undefined" && process.emitWarning) {
process.emitWarning("The 'eslint:recommended' string configuration is deprecated and will be replaced by the @eslint/js package's 'recommended' config.");
}
return jsPlugin.configs.recommended;
}
if (config === "eslint:all") {
// if we are in a Node.js environment warn the user
if (typeof process !== "undefined" && process.emitWarning) {
process.emitWarning("The 'eslint:all' string configuration is deprecated and will be replaced by the @eslint/js package's 'all' config.");
}
return jsPlugin.configs.all;
}
/*
* If `shouldIgnore` is false, we remove any ignore patterns specified
* in the config so long as it's not a default config and it doesn't
* have a `files` entry.
* If a config object has `ignores` and no other non-meta fields, then it's an object
* for global ignores. If `shouldIgnore` is false, that object shouldn't apply,
* so we'll remove its `ignores`.
*/
if (
!this.shouldIgnore &&
!this[originalBaseConfig].includes(config) &&
config.ignores &&
!config.files
Object.keys(config).filter(key => !META_FIELDS.has(key)).length === 1
) {
/* eslint-disable-next-line no-unused-vars -- need to strip off other keys */
const { ignores, ...otherKeys } = config;

View file

@ -5,6 +5,23 @@
"use strict";
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../shared/types").Rule} Rule */
//------------------------------------------------------------------------------
// Private Members
//------------------------------------------------------------------------------
// JSON schema that disallows passing any options
const noOptionsSchema = Object.freeze({
type: "array",
minItems: 0,
maxItems: 0
});
//-----------------------------------------------------------------------------
// Functions
//-----------------------------------------------------------------------------
@ -52,32 +69,39 @@ function getRuleFromConfig(ruleId, config) {
const { pluginName, ruleName } = parseRuleId(ruleId);
const plugin = config.plugins && config.plugins[pluginName];
let rule = plugin && plugin.rules && plugin.rules[ruleName];
// normalize function rules into objects
if (rule && typeof rule === "function") {
rule = {
create: rule
};
}
const rule = plugin && plugin.rules && plugin.rules[ruleName];
return rule;
}
/**
* Gets a complete options schema for a rule.
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
* @returns {Object} JSON Schema for the rule's options.
* @param {Rule} rule A rule object
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`.
* @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`.
*/
function getRuleOptionsSchema(rule) {
if (!rule) {
if (!rule.meta) {
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
}
const schema = rule.meta.schema;
if (typeof schema === "undefined") {
return { ...noOptionsSchema }; // default if `meta.schema` is not specified
}
// `schema:false` is an allowed explicit opt-out of options validation for the rule
if (schema === false) {
return null;
}
const schema = rule.schema || rule.meta && rule.meta.schema;
if (typeof schema !== "object" || schema === null) {
throw new TypeError("Rule's `meta.schema` must be an array or object");
}
// ESLint-specific array form needs to be converted into a valid JSON Schema definition
if (Array.isArray(schema)) {
if (schema.length) {
return {
@ -87,16 +111,13 @@ function getRuleOptionsSchema(rule) {
maxItems: schema.length
};
}
return {
type: "array",
minItems: 0,
maxItems: 0
};
// `schema:[]` is an explicit way to specify that the rule does not accept any options
return { ...noOptionsSchema };
}
// Given a full schema, leave it alone
return schema || null;
// `schema:<object>` is assumed to be a valid JSON Schema definition
return schema;
}

View file

@ -9,11 +9,6 @@
// Requirements
//-----------------------------------------------------------------------------
/*
* Note: This can be removed in ESLint v9 because structuredClone is available globally
* starting in Node.js v17.
*/
const structuredClone = require("@ungap/structured-clone").default;
const { normalizeSeverityToNumber } = require("../shared/severity");
//-----------------------------------------------------------------------------
@ -593,6 +588,5 @@ const flatConfigSchema = {
module.exports = {
flatConfigSchema,
assertIsRuleSeverity,
assertIsRuleOptions
assertIsRuleSeverity
};

View file

@ -66,6 +66,25 @@ function throwRuleNotFoundError({ pluginName, ruleName }, config) {
throw new TypeError(errorMessage);
}
/**
* The error type when a rule has an invalid `meta.schema`.
*/
class InvalidRuleOptionsSchemaError extends Error {
/**
* Creates a new instance.
* @param {string} ruleId Id of the rule that has an invalid `meta.schema`.
* @param {Error} processingError Error caught while processing the `meta.schema`.
*/
constructor(ruleId, processingError) {
super(
`Error while processing options validation schema of rule '${ruleId}': ${processingError.message}`,
{ cause: processingError }
);
this.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA";
}
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
@ -130,10 +149,14 @@ class RuleValidator {
// Precompile and cache validator the first time
if (!this.validators.has(rule)) {
const schema = getRuleOptionsSchema(rule);
try {
const schema = getRuleOptionsSchema(rule);
if (schema) {
this.validators.set(rule, ajv.compile(schema));
if (schema) {
this.validators.set(rule, ajv.compile(schema));
}
} catch (err) {
throw new InvalidRuleOptionsSchemaError(ruleId, err);
}
}
@ -144,9 +167,22 @@ class RuleValidator {
validateRule(ruleOptions.slice(1));
if (validateRule.errors) {
throw new Error(`Key "rules": Key "${ruleId}": ${
throw new Error(`Key "rules": Key "${ruleId}":\n${
validateRule.errors.map(
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
error => {
if (
error.keyword === "additionalProperties" &&
error.schema === false &&
typeof error.parentSchema?.properties === "object" &&
typeof error.params?.additionalProperty === "string"
) {
const expectedProperties = Object.keys(error.parentSchema.properties).map(property => `"${property}"`);
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`;
}
return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`;
}
).join("")
}`);
}

View file

@ -9,13 +9,12 @@
// Requirements
//-----------------------------------------------------------------------------
const path = require("path");
const fs = require("fs");
const path = require("node:path");
const fs = require("node:fs");
const fsp = fs.promises;
const isGlob = require("is-glob");
const hash = require("../cli-engine/hash");
const minimatch = require("minimatch");
const util = require("util");
const fswalk = require("@nodelib/fs.walk");
const globParent = require("glob-parent");
const isPathInside = require("is-path-inside");
@ -24,7 +23,6 @@ const isPathInside = require("is-path-inside");
// Fixup references
//-----------------------------------------------------------------------------
const doFsWalk = util.promisify(fswalk.walk);
const Minimatch = minimatch.Minimatch;
const MINIMATCH_OPTIONS = { dot: true };
@ -105,20 +103,30 @@ class AllFilesIgnoredError extends Error {
/**
* Check if a given value is a non-empty string or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is a non-empty string.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is a non-empty string.
*/
function isNonEmptyString(x) {
return typeof x === "string" && x.trim() !== "";
function isNonEmptyString(value) {
return typeof value === "string" && value.trim() !== "";
}
/**
* Check if a given value is an array of non-empty strings or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of non-empty strings.
*/
function isArrayOfNonEmptyString(x) {
return Array.isArray(x) && x.every(isNonEmptyString);
function isArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.length && value.every(isNonEmptyString);
}
/**
* Check if a given value is an empty array or an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an empty array or an array of non-empty
* strings.
*/
function isEmptyArrayOrArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.every(isNonEmptyString);
}
//-----------------------------------------------------------------------------
@ -270,56 +278,92 @@ async function globSearch({
*/
const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);
const filePaths = (await doFsWalk(basePath, {
const filePaths = (await new Promise((resolve, reject) => {
deepFilter(entry) {
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
let promiseRejected = false;
return matchesPattern && !configs.isDirectoryIgnored(entry.path);
},
entryFilter(entry) {
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
/**
* Wraps a boolean-returning filter function. The wrapped function will reject the promise if an error occurs.
* @param {Function} filter A filter function to wrap.
* @returns {Function} A function similar to the wrapped filter that rejects the promise if an error occurs.
*/
function wrapFilter(filter) {
return (...args) => {
// entries may be directories or files so filter out directories
if (entry.dirent.isDirectory()) {
return false;
}
/*
* Optimization: We need to track when patterns are left unmatched
* and so we use `unmatchedPatterns` to do that. There is a bit of
* complexity here because the same file can be matched by more than
* one pattern. So, when we start, we actually need to test every
* pattern against every file. Once we know there are no remaining
* unmatched patterns, then we can switch to just looking for the
* first matching pattern for improved speed.
*/
const matchesPattern = unmatchedPatterns.size > 0
? matchers.reduce((previousValue, matcher) => {
const pathMatches = matcher.match(relativePath);
/*
* We updated the unmatched patterns set only if the path
* matches and the file isn't ignored. If the file is
* ignored, that means there wasn't a match for the
* pattern so it should not be removed.
*
* Performance note: isFileIgnored() aggressively caches
* results so there is no performance penalty for calling
* it twice with the same argument.
*/
if (pathMatches && !configs.isFileIgnored(entry.path)) {
unmatchedPatterns.delete(matcher.pattern);
// No need to run the filter if an error has been thrown.
if (!promiseRejected) {
try {
return filter(...args);
} catch (error) {
promiseRejected = true;
reject(error);
}
return pathMatches || previousValue;
}, false)
: matchers.some(matcher => matcher.match(relativePath));
return matchesPattern && !configs.isFileIgnored(entry.path);
}
return false;
};
}
fswalk.walk(
basePath,
{
deepFilter: wrapFilter(entry => {
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
return matchesPattern && !configs.isDirectoryIgnored(entry.path);
}),
entryFilter: wrapFilter(entry => {
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
// entries may be directories or files so filter out directories
if (entry.dirent.isDirectory()) {
return false;
}
/*
* Optimization: We need to track when patterns are left unmatched
* and so we use `unmatchedPatterns` to do that. There is a bit of
* complexity here because the same file can be matched by more than
* one pattern. So, when we start, we actually need to test every
* pattern against every file. Once we know there are no remaining
* unmatched patterns, then we can switch to just looking for the
* first matching pattern for improved speed.
*/
const matchesPattern = unmatchedPatterns.size > 0
? matchers.reduce((previousValue, matcher) => {
const pathMatches = matcher.match(relativePath);
/*
* We updated the unmatched patterns set only if the path
* matches and the file isn't ignored. If the file is
* ignored, that means there wasn't a match for the
* pattern so it should not be removed.
*
* Performance note: isFileIgnored() aggressively caches
* results so there is no performance penalty for calling
* it twice with the same argument.
*/
if (pathMatches && !configs.isFileIgnored(entry.path)) {
unmatchedPatterns.delete(matcher.pattern);
}
return pathMatches || previousValue;
}, false)
: matchers.some(matcher => matcher.match(relativePath));
return matchesPattern && !configs.isFileIgnored(entry.path);
})
},
(error, entries) => {
// If the promise is already rejected, calling `resolve` or `reject` will do nothing.
if (error) {
reject(error);
} else {
resolve(entries);
}
}
);
})).map(entry => entry.path);
// now check to see if we have any unmatched patterns
@ -655,9 +699,9 @@ class ESLintInvalidOptionsError extends Error {
/**
* Validates and normalizes options for the wrapped CLIEngine instance.
* @param {FlatESLintOptions} options The options to process.
* @param {ESLintOptions} options The options to process.
* @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
* @returns {FlatESLintOptions} The normalized options.
* @returns {ESLintOptions} The normalized options.
*/
function processOptions({
allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
@ -675,7 +719,10 @@ function processOptions({
overrideConfig = null,
overrideConfigFile = null,
plugins = {},
stats = false,
warnIgnored = true,
passOnNoPatterns = false,
ruleFilter = () => true,
...unknownOptions
}) {
const errors = [];
@ -759,7 +806,7 @@ function processOptions({
if (typeof ignore !== "boolean") {
errors.push("'ignore' must be a boolean.");
}
if (!isArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) {
if (!isEmptyArrayOrArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) {
errors.push("'ignorePatterns' must be an array of non-empty strings or null.");
}
if (typeof overrideConfig !== "object") {
@ -768,6 +815,9 @@ function processOptions({
if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true) {
errors.push("'overrideConfigFile' must be a non-empty string, null, or true.");
}
if (typeof passOnNoPatterns !== "boolean") {
errors.push("'passOnNoPatterns' must be a boolean.");
}
if (typeof plugins !== "object") {
errors.push("'plugins' must be an object or null.");
} else if (plugins !== null && Object.keys(plugins).includes("")) {
@ -776,9 +826,15 @@ function processOptions({
if (Array.isArray(plugins)) {
errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.");
}
if (typeof stats !== "boolean") {
errors.push("'stats' must be a boolean.");
}
if (typeof warnIgnored !== "boolean") {
errors.push("'warnIgnored' must be a boolean.");
}
if (typeof ruleFilter !== "function") {
errors.push("'ruleFilter' must be a function.");
}
if (errors.length > 0) {
throw new ESLintInvalidOptionsError(errors);
}
@ -800,7 +856,10 @@ function processOptions({
globInputPaths,
ignore,
ignorePatterns,
warnIgnored
stats,
passOnNoPatterns,
warnIgnored,
ruleFilter
};
}
@ -887,7 +946,6 @@ function getCacheFile(cacheFile, cwd) {
//-----------------------------------------------------------------------------
module.exports = {
isGlobPattern,
findFiles,
isNonEmptyString,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
"use strict";
const { ESLint } = require("./eslint");
const { FlatESLint } = require("./flat-eslint");
const { LegacyESLint } = require("./legacy-eslint");
module.exports = {
ESLint,
FlatESLint
LegacyESLint
};

728
tools/node_modules/eslint/lib/eslint/legacy-eslint.js generated vendored Normal file
View file

@ -0,0 +1,728 @@
/**
* @fileoverview Main API Class
* @author Kai Cataldo
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const path = require("node:path");
const fs = require("node:fs");
const { promisify } = require("node:util");
const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine");
const BuiltinRules = require("../rules");
const {
Legacy: {
ConfigOps: {
getRuleSeverity
}
}
} = require("@eslint/eslintrc");
const { version } = require("../../package.json");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */
/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {import("../shared/types").Rule} Rule */
/** @typedef {import("../shared/types").LintResult} LintResult */
/** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
/**
* The main formatter object.
* @typedef LoadedFormatter
* @property {(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>} format format function.
*/
/**
* The options with which to configure the LegacyESLint instance.
* @typedef {Object} LegacyESLintOptions
* @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments.
* @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this instance
* @property {boolean} [cache] Enable result caching.
* @property {string} [cacheLocation] The cache file to use instead of .eslintcache.
* @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files.
* @property {string} [cwd] The value to use for the current working directory.
* @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`.
* @property {string[]} [extensions] An array of file extensions to check.
* @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean.
* @property {string[]} [fixTypes] Array of rule types to apply fixes for.
* @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
* @property {boolean} [ignore] False disables use of .eslintignore.
* @property {string} [ignorePath] The ignore file to use instead of .eslintignore.
* @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance
* @property {string} [overrideConfigFile] The configuration file to use.
* @property {Record<string,Plugin>|null} [plugins] Preloaded plugins. This is a map-like object, keys are plugin IDs and each value is implementation.
* @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives.
* @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD.
* @property {string[]} [rulePaths] An array of directories to load custom rules from.
* @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files.
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
* the linting operation to short circuit and not report any failures.
*/
/**
* A rules metadata object.
* @typedef {Object} RulesMeta
* @property {string} id The plugin ID.
* @property {Object} definition The plugin definition.
*/
/**
* Private members for the `ESLint` instance.
* @typedef {Object} ESLintPrivateMembers
* @property {CLIEngine} cliEngine The wrapped CLIEngine instance.
* @property {LegacyESLintOptions} options The options used to instantiate the ESLint instance.
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const writeFile = promisify(fs.writeFile);
/**
* The map with which to store private class members.
* @type {WeakMap<ESLint, ESLintPrivateMembers>}
*/
const privateMembersMap = new WeakMap();
/**
* Check if a given value is a non-empty string or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is a non-empty string.
*/
function isNonEmptyString(value) {
return typeof value === "string" && value.trim() !== "";
}
/**
* Check if a given value is an array of non-empty strings or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of non-empty strings.
*/
function isArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.length && value.every(isNonEmptyString);
}
/**
* Check if a given value is an empty array or an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an empty array or an array of non-empty
* strings.
*/
function isEmptyArrayOrArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.every(isNonEmptyString);
}
/**
* Check if a given value is a valid fix type or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is valid fix type.
*/
function isFixType(value) {
return value === "directive" || value === "problem" || value === "suggestion" || value === "layout";
}
/**
* Check if a given value is an array of fix types or not.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of fix types.
*/
function isFixTypeArray(value) {
return Array.isArray(value) && value.every(isFixType);
}
/**
* The error for invalid options.
*/
class ESLintInvalidOptionsError extends Error {
constructor(messages) {
super(`Invalid Options:\n- ${messages.join("\n- ")}`);
this.code = "ESLINT_INVALID_OPTIONS";
Error.captureStackTrace(this, ESLintInvalidOptionsError);
}
}
/**
* Validates and normalizes options for the wrapped CLIEngine instance.
* @param {LegacyESLintOptions} options The options to process.
* @throws {ESLintInvalidOptionsError} If of any of a variety of type errors.
* @returns {LegacyESLintOptions} The normalized options.
*/
function processOptions({
allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored.
baseConfig = null,
cache = false,
cacheLocation = ".eslintcache",
cacheStrategy = "metadata",
cwd = process.cwd(),
errorOnUnmatchedPattern = true,
extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature.
fix = false,
fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property.
globInputPaths = true,
ignore = true,
ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT.
overrideConfig = null,
overrideConfigFile = null,
plugins = {},
reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that.
resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature.
rulePaths = [],
useEslintrc = true,
passOnNoPatterns = false,
...unknownOptions
}) {
const errors = [];
const unknownOptionKeys = Object.keys(unknownOptions);
if (unknownOptionKeys.length >= 1) {
errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`);
if (unknownOptionKeys.includes("cacheFile")) {
errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead.");
}
if (unknownOptionKeys.includes("configFile")) {
errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead.");
}
if (unknownOptionKeys.includes("envs")) {
errors.push("'envs' has been removed. Please use the 'overrideConfig.env' option instead.");
}
if (unknownOptionKeys.includes("globals")) {
errors.push("'globals' has been removed. Please use the 'overrideConfig.globals' option instead.");
}
if (unknownOptionKeys.includes("ignorePattern")) {
errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.");
}
if (unknownOptionKeys.includes("parser")) {
errors.push("'parser' has been removed. Please use the 'overrideConfig.parser' option instead.");
}
if (unknownOptionKeys.includes("parserOptions")) {
errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead.");
}
if (unknownOptionKeys.includes("rules")) {
errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead.");
}
}
if (typeof allowInlineConfig !== "boolean") {
errors.push("'allowInlineConfig' must be a boolean.");
}
if (typeof baseConfig !== "object") {
errors.push("'baseConfig' must be an object or null.");
}
if (typeof cache !== "boolean") {
errors.push("'cache' must be a boolean.");
}
if (!isNonEmptyString(cacheLocation)) {
errors.push("'cacheLocation' must be a non-empty string.");
}
if (
cacheStrategy !== "metadata" &&
cacheStrategy !== "content"
) {
errors.push("'cacheStrategy' must be any of \"metadata\", \"content\".");
}
if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
errors.push("'cwd' must be an absolute path.");
}
if (typeof errorOnUnmatchedPattern !== "boolean") {
errors.push("'errorOnUnmatchedPattern' must be a boolean.");
}
if (!isEmptyArrayOrArrayOfNonEmptyString(extensions) && extensions !== null) {
errors.push("'extensions' must be an array of non-empty strings or null.");
}
if (typeof fix !== "boolean" && typeof fix !== "function") {
errors.push("'fix' must be a boolean or a function.");
}
if (fixTypes !== null && !isFixTypeArray(fixTypes)) {
errors.push("'fixTypes' must be an array of any of \"directive\", \"problem\", \"suggestion\", and \"layout\".");
}
if (typeof globInputPaths !== "boolean") {
errors.push("'globInputPaths' must be a boolean.");
}
if (typeof ignore !== "boolean") {
errors.push("'ignore' must be a boolean.");
}
if (!isNonEmptyString(ignorePath) && ignorePath !== null) {
errors.push("'ignorePath' must be a non-empty string or null.");
}
if (typeof overrideConfig !== "object") {
errors.push("'overrideConfig' must be an object or null.");
}
if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null) {
errors.push("'overrideConfigFile' must be a non-empty string or null.");
}
if (typeof plugins !== "object") {
errors.push("'plugins' must be an object or null.");
} else if (plugins !== null && Object.keys(plugins).includes("")) {
errors.push("'plugins' must not include an empty string.");
}
if (Array.isArray(plugins)) {
errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead.");
}
if (
reportUnusedDisableDirectives !== "error" &&
reportUnusedDisableDirectives !== "warn" &&
reportUnusedDisableDirectives !== "off" &&
reportUnusedDisableDirectives !== null
) {
errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.");
}
if (
!isNonEmptyString(resolvePluginsRelativeTo) &&
resolvePluginsRelativeTo !== null
) {
errors.push("'resolvePluginsRelativeTo' must be a non-empty string or null.");
}
if (!isEmptyArrayOrArrayOfNonEmptyString(rulePaths)) {
errors.push("'rulePaths' must be an array of non-empty strings.");
}
if (typeof useEslintrc !== "boolean") {
errors.push("'useEslintrc' must be a boolean.");
}
if (typeof passOnNoPatterns !== "boolean") {
errors.push("'passOnNoPatterns' must be a boolean.");
}
if (errors.length > 0) {
throw new ESLintInvalidOptionsError(errors);
}
return {
allowInlineConfig,
baseConfig,
cache,
cacheLocation,
cacheStrategy,
configFile: overrideConfigFile,
cwd: path.normalize(cwd),
errorOnUnmatchedPattern,
extensions,
fix,
fixTypes,
globInputPaths,
ignore,
ignorePath,
reportUnusedDisableDirectives,
resolvePluginsRelativeTo,
rulePaths,
useEslintrc,
passOnNoPatterns
};
}
/**
* Check if a value has one or more properties and that value is not undefined.
* @param {any} obj The value to check.
* @returns {boolean} `true` if `obj` has one or more properties that that value is not undefined.
*/
function hasDefinedProperty(obj) {
if (typeof obj === "object" && obj !== null) {
for (const key in obj) {
if (typeof obj[key] !== "undefined") {
return true;
}
}
}
return false;
}
/**
* Create rulesMeta object.
* @param {Map<string,Rule>} rules a map of rules from which to generate the object.
* @returns {Object} metadata for all enabled rules.
*/
function createRulesMeta(rules) {
return Array.from(rules).reduce((retVal, [id, rule]) => {
retVal[id] = rule.meta;
return retVal;
}, {});
}
/** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */
const usedDeprecatedRulesCache = new WeakMap();
/**
* Create used deprecated rule list.
* @param {CLIEngine} cliEngine The CLIEngine instance.
* @param {string} maybeFilePath The absolute path to a lint target file or `"<text>"`.
* @returns {DeprecatedRuleInfo[]} The used deprecated rule list.
*/
function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) {
const {
configArrayFactory,
options: { cwd }
} = getCLIEngineInternalSlots(cliEngine);
const filePath = path.isAbsolute(maybeFilePath)
? maybeFilePath
: path.join(cwd, "__placeholder__.js");
const configArray = configArrayFactory.getConfigArrayForFile(filePath);
const config = configArray.extractConfig(filePath);
// Most files use the same config, so cache it.
if (!usedDeprecatedRulesCache.has(config)) {
const pluginRules = configArray.pluginRules;
const retv = [];
for (const [ruleId, ruleConf] of Object.entries(config.rules)) {
if (getRuleSeverity(ruleConf) === 0) {
continue;
}
const rule = pluginRules.get(ruleId) || BuiltinRules.get(ruleId);
const meta = rule && rule.meta;
if (meta && meta.deprecated) {
retv.push({ ruleId, replacedBy: meta.replacedBy || [] });
}
}
usedDeprecatedRulesCache.set(config, Object.freeze(retv));
}
return usedDeprecatedRulesCache.get(config);
}
/**
* Processes the linting results generated by a CLIEngine linting report to
* match the ESLint class's API.
* @param {CLIEngine} cliEngine The CLIEngine instance.
* @param {CLIEngineLintReport} report The CLIEngine linting report to process.
* @returns {LintResult[]} The processed linting results.
*/
function processCLIEngineLintReport(cliEngine, { results }) {
const descriptor = {
configurable: true,
enumerable: true,
get() {
return getOrFindUsedDeprecatedRules(cliEngine, this.filePath);
}
};
for (const result of results) {
Object.defineProperty(result, "usedDeprecatedRules", descriptor);
}
return results;
}
/**
* An Array.prototype.sort() compatible compare function to order results by their file path.
* @param {LintResult} a The first lint result.
* @param {LintResult} b The second lint result.
* @returns {number} An integer representing the order in which the two results should occur.
*/
function compareResultsByFilePath(a, b) {
if (a.filePath < b.filePath) {
return -1;
}
if (a.filePath > b.filePath) {
return 1;
}
return 0;
}
/**
* Main API.
*/
class LegacyESLint {
/**
* The type of configuration used by this class.
* @type {string}
*/
static configType = "eslintrc";
/**
* Creates a new instance of the main ESLint API.
* @param {LegacyESLintOptions} options The options for this instance.
*/
constructor(options = {}) {
const processedOptions = processOptions(options);
const cliEngine = new CLIEngine(processedOptions, { preloadedPlugins: options.plugins });
const {
configArrayFactory,
lastConfigArrays
} = getCLIEngineInternalSlots(cliEngine);
let updated = false;
/*
* Address `overrideConfig` to set override config.
* Operate the `configArrayFactory` internal slot directly because this
* functionality doesn't exist as the public API of CLIEngine.
*/
if (hasDefinedProperty(options.overrideConfig)) {
configArrayFactory.setOverrideConfig(options.overrideConfig);
updated = true;
}
// Update caches.
if (updated) {
configArrayFactory.clearCache();
lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile();
}
// Initialize private properties.
privateMembersMap.set(this, {
cliEngine,
options: processedOptions
});
}
/**
* The version text.
* @type {string}
*/
static get version() {
return version;
}
/**
* Outputs fixes from the given results to files.
* @param {LintResult[]} results The lint results.
* @returns {Promise<void>} Returns a promise that is used to track side effects.
*/
static async outputFixes(results) {
if (!Array.isArray(results)) {
throw new Error("'results' must be an array");
}
await Promise.all(
results
.filter(result => {
if (typeof result !== "object" || result === null) {
throw new Error("'results' must include only objects");
}
return (
typeof result.output === "string" &&
path.isAbsolute(result.filePath)
);
})
.map(r => writeFile(r.filePath, r.output))
);
}
/**
* Returns results that only contains errors.
* @param {LintResult[]} results The results to filter.
* @returns {LintResult[]} The filtered results.
*/
static getErrorResults(results) {
return CLIEngine.getErrorResults(results);
}
/**
* Returns meta objects for each rule represented in the lint results.
* @param {LintResult[]} results The results to fetch rules meta for.
* @returns {Object} A mapping of ruleIds to rule meta objects.
*/
getRulesMetaForResults(results) {
const resultRuleIds = new Set();
// first gather all ruleIds from all results
for (const result of results) {
for (const { ruleId } of result.messages) {
resultRuleIds.add(ruleId);
}
for (const { ruleId } of result.suppressedMessages) {
resultRuleIds.add(ruleId);
}
}
// create a map of all rules in the results
const { cliEngine } = privateMembersMap.get(this);
const rules = cliEngine.getRules();
const resultRules = new Map();
for (const [ruleId, rule] of rules) {
if (resultRuleIds.has(ruleId)) {
resultRules.set(ruleId, rule);
}
}
return createRulesMeta(resultRules);
}
/**
* Executes the current configuration on an array of file and directory names.
* @param {string[]} patterns An array of file and directory names.
* @returns {Promise<LintResult[]>} The results of linting the file patterns given.
*/
async lintFiles(patterns) {
const { cliEngine, options } = privateMembersMap.get(this);
if (options.passOnNoPatterns && (patterns === "" || (Array.isArray(patterns) && patterns.length === 0))) {
return [];
}
if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
throw new Error("'patterns' must be a non-empty string or an array of non-empty strings");
}
return processCLIEngineLintReport(
cliEngine,
cliEngine.executeOnFiles(patterns)
);
}
/**
* Executes the current configuration on text.
* @param {string} code A string of JavaScript code to lint.
* @param {Object} [options] The options.
* @param {string} [options.filePath] The path to the file of the source code.
* @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path.
* @returns {Promise<LintResult[]>} The results of linting the string of code given.
*/
async lintText(code, options = {}) {
if (typeof code !== "string") {
throw new Error("'code' must be a string");
}
if (typeof options !== "object") {
throw new Error("'options' must be an object, null, or undefined");
}
const {
filePath,
warnIgnored = false,
...unknownOptions
} = options || {};
const unknownOptionKeys = Object.keys(unknownOptions);
if (unknownOptionKeys.length > 0) {
throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`);
}
if (filePath !== void 0 && !isNonEmptyString(filePath)) {
throw new Error("'options.filePath' must be a non-empty string or undefined");
}
if (typeof warnIgnored !== "boolean") {
throw new Error("'options.warnIgnored' must be a boolean or undefined");
}
const { cliEngine } = privateMembersMap.get(this);
return processCLIEngineLintReport(
cliEngine,
cliEngine.executeOnText(code, filePath, warnIgnored)
);
}
/**
* Returns the formatter representing the given formatter name.
* @param {string} [name] The name of the formatter to load.
* The following values are allowed:
* - `undefined` ... Load `stylish` builtin formatter.
* - A builtin formatter name ... Load the builtin formatter.
* - A third-party formatter name:
* - `foo` `eslint-formatter-foo`
* - `@foo` `@foo/eslint-formatter`
* - `@foo/bar` `@foo/eslint-formatter-bar`
* - A file path ... Load the file.
* @returns {Promise<LoadedFormatter>} A promise resolving to the formatter object.
* This promise will be rejected if the given formatter was not found or not
* a function.
*/
async loadFormatter(name = "stylish") {
if (typeof name !== "string") {
throw new Error("'name' must be a string");
}
const { cliEngine, options } = privateMembersMap.get(this);
const formatter = cliEngine.getFormatter(name);
if (typeof formatter !== "function") {
throw new Error(`Formatter must be a function, but got a ${typeof formatter}.`);
}
return {
/**
* The main formatter method.
* @param {LintResult[]} results The lint results to format.
* @param {ResultsMeta} resultsMeta Warning count and max threshold.
* @returns {string | Promise<string>} The formatted lint results.
*/
format(results, resultsMeta) {
let rulesMeta = null;
results.sort(compareResultsByFilePath);
return formatter(results, {
...resultsMeta,
get cwd() {
return options.cwd;
},
get rulesMeta() {
if (!rulesMeta) {
rulesMeta = createRulesMeta(cliEngine.getRules());
}
return rulesMeta;
}
});
}
};
}
/**
* Returns a configuration object for the given file based on the CLI options.
* This is the same logic used by the ESLint CLI executable to determine
* configuration for each file it processes.
* @param {string} filePath The path of the file to retrieve a config object for.
* @returns {Promise<ConfigData>} A configuration object for the file.
*/
async calculateConfigForFile(filePath) {
if (!isNonEmptyString(filePath)) {
throw new Error("'filePath' must be a non-empty string");
}
const { cliEngine } = privateMembersMap.get(this);
return cliEngine.getConfigForFile(filePath);
}
/**
* Checks if a given path is ignored by ESLint.
* @param {string} filePath The path of the file to check.
* @returns {Promise<boolean>} Whether or not the given path is ignored.
*/
async isPathIgnored(filePath) {
if (!isNonEmptyString(filePath)) {
throw new Error("'filePath' must be a non-empty string");
}
const { cliEngine } = privateMembersMap.get(this);
return cliEngine.isPathIgnored(filePath);
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
LegacyESLint,
/**
* Get the private class members of a given ESLint instance for tests.
* @param {ESLint} instance The ESLint instance to get.
* @returns {ESLintPrivateMembers} The instance's private class members.
*/
getESLintPrivateMembers(instance) {
return privateMembersMap.get(instance);
}
};

View file

@ -16,6 +16,11 @@
//------------------------------------------------------------------------------
const escapeRegExp = require("escape-string-regexp");
const {
Legacy: {
ConfigOps
}
} = require("@eslint/eslintrc/universal");
/**
* Compares the locations of two objects in a source file
@ -33,16 +38,16 @@ function compareLocations(itemA, itemB) {
* @param {Iterable<Directive>} directives Unused directives to be removed.
* @returns {Directive[][]} Directives grouped by their parent comment.
*/
function groupByParentComment(directives) {
function groupByParentDirective(directives) {
const groups = new Map();
for (const directive of directives) {
const { unprocessedDirective: { parentComment } } = directive;
const { unprocessedDirective: { parentDirective } } = directive;
if (groups.has(parentComment)) {
groups.get(parentComment).push(directive);
if (groups.has(parentDirective)) {
groups.get(parentDirective).push(directive);
} else {
groups.set(parentComment, [directive]);
groups.set(parentDirective, [directive]);
}
}
@ -52,19 +57,19 @@ function groupByParentComment(directives) {
/**
* Creates removal details for a set of directives within the same comment.
* @param {Directive[]} directives Unused directives to be removed.
* @param {Token} commentToken The backing Comment token.
* @param {Token} node The backing Comment token.
* @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
*/
function createIndividualDirectivesRemoval(directives, commentToken) {
function createIndividualDirectivesRemoval(directives, node) {
/*
* `commentToken.value` starts right after `//` or `/*`.
* `node.value` starts right after `//` or `/*`.
* All calculated offsets will be relative to this index.
*/
const commentValueStart = commentToken.range[0] + "//".length;
const commentValueStart = node.range[0] + "//".length;
// Find where the list of rules starts. `\S+` matches with the directive name (e.g. `eslint-disable-line`)
const listStartOffset = /^\s*\S+\s+/u.exec(commentToken.value)[0].length;
const listStartOffset = /^\s*\S+\s+/u.exec(node.value)[0].length;
/*
* Get the list text without any surrounding whitespace. In order to preserve the original
@ -73,7 +78,7 @@ function createIndividualDirectivesRemoval(directives, commentToken) {
* // eslint-disable-line rule-one , rule-two , rule-three -- comment
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*/
const listText = commentToken.value
const listText = node.value
.slice(listStartOffset) // remove directive name and all whitespace before the list
.split(/\s-{2,}\s/u)[0] // remove `-- comment`, if it exists
.trimEnd(); // remove all whitespace after the list
@ -154,19 +159,19 @@ function createIndividualDirectivesRemoval(directives, commentToken) {
}
/**
* Creates a description of deleting an entire unused disable comment.
* Creates a description of deleting an entire unused disable directive.
* @param {Directive[]} directives Unused directives to be removed.
* @param {Token} commentToken The backing Comment token.
* @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output Problem.
* @param {Token} node The backing Comment token.
* @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output problem.
*/
function createCommentRemoval(directives, commentToken) {
const { range } = commentToken;
function createDirectiveRemoval(directives, node) {
const { range } = node;
const ruleIds = directives.filter(directive => directive.ruleId).map(directive => `'${directive.ruleId}'`);
return {
description: ruleIds.length <= 2
? ruleIds.join(" or ")
: `${ruleIds.slice(0, ruleIds.length - 1).join(", ")}, or ${ruleIds[ruleIds.length - 1]}`,
: `${ruleIds.slice(0, ruleIds.length - 1).join(", ")}, or ${ruleIds.at(-1)}`,
fix: {
range,
text: " "
@ -181,20 +186,20 @@ function createCommentRemoval(directives, commentToken) {
* @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
*/
function processUnusedDirectives(allDirectives) {
const directiveGroups = groupByParentComment(allDirectives);
const directiveGroups = groupByParentDirective(allDirectives);
return directiveGroups.flatMap(
directives => {
const { parentComment } = directives[0].unprocessedDirective;
const remainingRuleIds = new Set(parentComment.ruleIds);
const { parentDirective } = directives[0].unprocessedDirective;
const remainingRuleIds = new Set(parentDirective.ruleIds);
for (const directive of directives) {
remainingRuleIds.delete(directive.ruleId);
}
return remainingRuleIds.size
? createIndividualDirectivesRemoval(directives, parentComment.commentToken)
: [createCommentRemoval(directives, parentComment.commentToken)];
? createIndividualDirectivesRemoval(directives, parentDirective.node)
: [createDirectiveRemoval(directives, parentDirective.node)];
}
);
}
@ -337,7 +342,7 @@ function applyDirectives(options) {
problem.suppressions = problem.suppressions.concat(suppressions);
} else {
problem.suppressions = suppressions;
usedDisableDirectives.add(disableDirectivesForProblem[disableDirectivesForProblem.length - 1]);
usedDisableDirectives.add(disableDirectivesForProblem.at(-1));
}
}
@ -345,11 +350,11 @@ function applyDirectives(options) {
}
const unusedDisableDirectivesToReport = options.directives
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive));
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive) && !options.rulesToIgnore.has(directive.ruleId));
const unusedEnableDirectivesToReport = new Set(
options.directives.filter(directive => directive.unprocessedDirective.type === "enable")
options.directives.filter(directive => directive.unprocessedDirective.type === "enable" && !options.rulesToIgnore.has(directive.ruleId))
);
/*
@ -367,7 +372,7 @@ function applyDirectives(options) {
const unusedDirectives = processed
.map(({ description, fix, unprocessedDirective }) => {
const { parentComment, type, line, column } = unprocessedDirective;
const { parentDirective, type, line, column } = unprocessedDirective;
let message;
@ -383,8 +388,8 @@ function applyDirectives(options) {
return {
ruleId: null,
message,
line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line,
column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column,
line: type === "disable-next-line" ? parentDirective.node.loc.start.line : line,
column: type === "disable-next-line" ? parentDirective.node.loc.start.column + 1 : column,
severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
nodeType: null,
...options.disableFixes ? {} : { fix }
@ -410,11 +415,13 @@ function applyDirectives(options) {
* @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
* A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
* @param {"off" | "warn" | "error"} options.reportUnusedDisableDirectives If `"warn"` or `"error"`, adds additional problems for unused directives
* @param {Object} options.configuredRules The rules configuration.
* @param {Function} options.ruleFilter A predicate function to filter which rules should be executed.
* @param {boolean} options.disableFixes If true, it doesn't make `fix` properties.
* @returns {{ruleId: (string|null), line: number, column: number, suppressions?: {kind: string, justification: string}}[]}
* An object with a list of reported problems, the suppressed of which contain the suppression information.
*/
module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirectives = "off" }) => {
module.exports = ({ directives, disableFixes, problems, configuredRules, ruleFilter, reportUnusedDisableDirectives = "off" }) => {
const blockDirectives = directives
.filter(directive => directive.type === "disable" || directive.type === "enable")
.map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
@ -443,17 +450,38 @@ module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirec
}
}).sort(compareLocations);
// This determines a list of rules that are not being run by the given ruleFilter, if present.
const rulesToIgnore = configuredRules && ruleFilter
? new Set(Object.keys(configuredRules).filter(ruleId => {
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// Ignore for disabled rules.
if (severity === 0) {
return false;
}
return !ruleFilter({ severity, ruleId });
}))
: new Set();
// If no ruleId is supplied that means this directive is applied to all rules, so we can't determine if it's unused if any rules are filtered out.
if (rulesToIgnore.size > 0) {
rulesToIgnore.add(null);
}
const blockDirectivesResult = applyDirectives({
problems,
directives: blockDirectives,
disableFixes,
reportUnusedDisableDirectives
reportUnusedDisableDirectives,
rulesToIgnore
});
const lineDirectivesResult = applyDirectives({
problems: blockDirectivesResult.problems,
directives: lineDirectives,
disableFixes,
reportUnusedDisableDirectives
reportUnusedDisableDirectives,
rulesToIgnore
});
return reportUnusedDisableDirectives !== "off"

View file

@ -9,7 +9,7 @@
// Requirements
//------------------------------------------------------------------------------
const assert = require("assert"),
const assert = require("node:assert"),
{ breakableTypePattern } = require("../../shared/ast-utils"),
CodePath = require("./code-path"),
CodePathSegment = require("./code-path-segment"),
@ -222,7 +222,6 @@ function forwardCurrentToHead(analyzer, node) {
: "onUnreachableCodePathSegmentStart";
debug.dump(`${eventName} ${headSegment.id}`);
CodePathSegment.markUsed(headSegment);
analyzer.emitter.emit(
eventName,

View file

@ -125,20 +125,6 @@ class CodePath {
return this.internal.thrownForkContext;
}
/**
* Tracks the traversal of the code path through each segment. This array
* starts empty and segments are added or removed as the code path is
* traversed. This array always ends up empty at the end of a code path
* traversal. The `CodePathState` uses this to track its progress through
* the code path.
* This is a passthrough to the underlying `CodePathState`.
* @type {CodePathSegment[]}
* @deprecated
*/
get currentSegments() {
return this.internal.currentSegments;
}
/**
* Traverses all segments in this code path.
*
@ -180,9 +166,9 @@ class CodePath {
const lastSegment = resolvedOptions.last;
// set up initial location information
let record = null;
let index = 0;
let end = 0;
let record;
let index;
let end;
let segment = null;
// segments that have already been visited during traversal
@ -191,8 +177,8 @@ class CodePath {
// tracks the traversal steps
const stack = [[startSegment, 0]];
// tracks the last skipped segment during traversal
let skippedSegment = null;
// segments that have been skipped during traversal
const skipped = new Set();
// indicates if we exited early from the traversal
let broken = false;
@ -207,11 +193,7 @@ class CodePath {
* @returns {void}
*/
skip() {
if (stack.length <= 1) {
broken = true;
} else {
skippedSegment = stack[stack.length - 2][0];
}
skipped.add(segment);
},
/**
@ -236,6 +218,18 @@ class CodePath {
);
}
/**
* Checks if a given previous segment has been skipped.
* @param {CodePathSegment} prevSegment A previous segment to check.
* @returns {boolean} `true` if the segment has been skipped.
*/
function isSkipped(prevSegment) {
return (
skipped.has(prevSegment) ||
segment.isLoopedPrevSegment(prevSegment)
);
}
// the traversal
while (stack.length > 0) {
@ -251,7 +245,7 @@ class CodePath {
* Otherwise, we just read the value and sometimes modify the
* record as we traverse.
*/
record = stack[stack.length - 1];
record = stack.at(-1);
segment = record[0];
index = record[1];
@ -272,17 +266,21 @@ class CodePath {
continue;
}
// Reset the skipping flag if all branches have been skipped.
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
skippedSegment = null;
}
visited.add(segment);
// Skips the segment if all previous segments have been skipped.
const shouldSkip = (
skipped.size > 0 &&
segment.prevSegments.length > 0 &&
segment.prevSegments.every(isSkipped)
);
/*
* If the most recent segment hasn't been skipped, then we call
* the callback, passing in the segment and the controller.
*/
if (!skippedSegment) {
if (!shouldSkip) {
resolvedCallback.call(this, segment, controller);
// exit if we're at the last segment
@ -298,6 +296,10 @@ class CodePath {
if (broken) {
break;
}
} else {
// If the most recent segment has been skipped, then mark it as skipped.
skipped.add(segment);
}
}

View file

@ -13,7 +13,7 @@
// Requirements
//------------------------------------------------------------------------------
const assert = require("assert"),
const assert = require("node:assert"),
CodePathSegment = require("./code-path-segment");
//------------------------------------------------------------------------------
@ -207,7 +207,7 @@ class ForkContext {
get head() {
const list = this.segmentsList;
return list.length === 0 ? [] : list[list.length - 1];
return list.length === 0 ? [] : list.at(-1);
}
/**

View file

@ -40,7 +40,7 @@ module.exports = class ConfigCommentParser {
/**
* Parses a list of "name:string_value" or/and "name" options divided by comma or
* whitespace. Used for "global" and "exported" comments.
* whitespace. Used for "global" comments.
* @param {string} string The string to parse.
* @param {Comment} comment The comment node which has the string.
* @returns {Object} Result map object of names and string values, or null values if no value was provided
@ -75,11 +75,9 @@ module.exports = class ConfigCommentParser {
parseJsonConfig(string, location) {
debug("Parsing JSON config");
let items = {};
// Parses a JSON-like comment by the same way as parsing CLI option.
try {
items = levn.parse("Object", string) || {};
const items = levn.parse("Object", string) || {};
// Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`.
// Also, commaless notations have invalid severity:
@ -102,11 +100,15 @@ module.exports = class ConfigCommentParser {
* Optionator cannot parse commaless notations.
* But we are supporting that. So this is a fallback for that.
*/
items = {};
const normalizedString = string.replace(/([-a-zA-Z0-9/]+):/gu, "\"$1\":").replace(/(\]|[0-9])\s+(?=")/u, "$1,");
try {
items = JSON.parse(`{${normalizedString}}`);
const items = JSON.parse(`{${normalizedString}}`);
return {
success: true,
config: items
};
} catch (ex) {
debug("Manual parsing failed.");
@ -124,11 +126,6 @@ module.exports = class ConfigCommentParser {
};
}
return {
success: true,
config: items
};
}
/**

View file

@ -1,13 +1,11 @@
"use strict";
const { Linter } = require("./linter");
const interpolate = require("./interpolate");
const SourceCodeFixer = require("./source-code-fixer");
module.exports = {
Linter,
// For testers.
SourceCodeFixer,
interpolate
SourceCodeFixer
};

View file

@ -9,13 +9,30 @@
// Public Interface
//------------------------------------------------------------------------------
module.exports = (text, data) => {
/**
* Returns a global expression matching placeholders in messages.
* @returns {RegExp} Global regular expression matching placeholders
*/
function getPlaceholderMatcher() {
return /\{\{([^{}]+?)\}\}/gu;
}
/**
* Replaces {{ placeholders }} in the message with the provided data.
* Does not replace placeholders not available in the data.
* @param {string} text Original message with potential placeholders
* @param {Record<string, string>} data Map of placeholder name to its value
* @returns {string} Message with replaced placeholders
*/
function interpolate(text, data) {
if (!data) {
return text;
}
const matcher = getPlaceholderMatcher();
// Substitution content for any {{ }} markers.
return text.replace(/\{\{([^{}]+?)\}\}/gu, (fullMatch, termWithWhitespace) => {
return text.replace(matcher, (fullMatch, termWithWhitespace) => {
const term = termWithWhitespace.trim();
if (term in data) {
@ -25,4 +42,9 @@ module.exports = (text, data) => {
// Preserve old behavior: If parameter name not provided, don't replace it.
return fullMatch;
});
}
module.exports = {
getPlaceholderMatcher,
interpolate
};

File diff suppressed because it is too large Load diff

View file

@ -9,9 +9,9 @@
// Requirements
//------------------------------------------------------------------------------
const assert = require("assert");
const assert = require("node:assert");
const ruleFixer = require("./rule-fixer");
const interpolate = require("./interpolate");
const { interpolate } = require("./interpolate");
//------------------------------------------------------------------------------
// Typedefs
@ -160,7 +160,7 @@ function mergeFixes(fixes, sourceCode) {
const originalText = sourceCode.text;
const start = fixes[0].range[0];
const end = fixes[fixes.length - 1].range[1];
const end = fixes.at(-1).range[1];
let text = "";
let lastPos = Number.MIN_SAFE_INTEGER;
@ -343,7 +343,7 @@ module.exports = function createReportTranslator(metadata) {
if (descriptor.message) {
throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
}
if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
if (!messages || !Object.hasOwn(messages, id)) {
throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
}
computedMessage = messages[id];

View file

@ -13,18 +13,10 @@
const builtInRules = require("../rules");
//------------------------------------------------------------------------------
// Helpers
// Typedefs
//------------------------------------------------------------------------------
/**
* Normalizes a rule module to the new-style API
* @param {(Function|{create: Function})} rule A rule object, which can either be a function
* ("old-style") or an object with a `create` method ("new-style")
* @returns {{create: Function}} A new-style rule.
*/
function normalizeRule(rule) {
return typeof rule === "function" ? Object.assign({ create: rule }, rule) : rule;
}
/** @typedef {import("../shared/types").Rule} Rule */
//------------------------------------------------------------------------------
// Public Interface
@ -41,18 +33,17 @@ class Rules {
/**
* Registers a rule module for rule id in storage.
* @param {string} ruleId Rule id (file name).
* @param {Function} ruleModule Rule handler.
* @param {Rule} rule Rule object.
* @returns {void}
*/
define(ruleId, ruleModule) {
this._rules[ruleId] = normalizeRule(ruleModule);
define(ruleId, rule) {
this._rules[ruleId] = rule;
}
/**
* Access rule handler by id (file name).
* @param {string} ruleId Rule id (file name).
* @returns {{create: Function, schema: JsonSchema[]}}
* A rule. This is normalized to always have the new-style shape with a `create` method.
* @returns {Rule} Rule object.
*/
get(ruleId) {
if (typeof this._rules[ruleId] === "string") {

View file

@ -107,7 +107,7 @@ SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
}
messages.forEach(problem => {
if (Object.prototype.hasOwnProperty.call(problem, "fix")) {
if (Object.hasOwn(problem, "fix")) {
fixes.push(problem);
} else {
remainingMessages.push(problem);

View file

@ -5,6 +5,8 @@
"use strict";
const { startTime, endTime } = require("../shared/stats");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
@ -128,21 +130,27 @@ module.exports = (function() {
* Time the run
* @param {any} key key from the data object
* @param {Function} fn function to be called
* @param {boolean} stats if 'stats' is true, return the result and the time difference
* @returns {Function} function to be executed
* @private
*/
function time(key, fn) {
if (typeof data[key] === "undefined") {
data[key] = 0;
}
function time(key, fn, stats) {
return function(...args) {
let t = process.hrtime();
const result = fn(...args);
t = process.hrtime(t);
data[key] += t[0] * 1e3 + t[1] / 1e6;
return result;
const t = startTime();
const result = fn(...args);
const tdiff = endTime(t);
if (enabled) {
if (typeof data[key] === "undefined") {
data[key] = 0;
}
data[key] += tdiff;
}
return stats ? { result, tdiff } : result;
};
}

View file

@ -38,7 +38,7 @@ const optionator = require("optionator");
* @property {boolean} [help] Show help
* @property {boolean} ignore Disable use of ignore files and patterns
* @property {string} [ignorePath] Specify path of ignore file
* @property {string[]} [ignorePattern] Pattern of files to ignore (in addition to those in .eslintignore)
* @property {string[]} [ignorePattern] Patterns of files to ignore. In eslintrc mode, these are in addition to `.eslintignore`
* @property {boolean} init Run config initialization wizard
* @property {boolean} inlineConfig Prevent comments from changing config or rules
* @property {number} maxWarnings Number of warnings to trigger nonzero exit code
@ -57,7 +57,10 @@ const optionator = require("optionator");
* @property {boolean} quiet Report errors only
* @property {boolean} [version] Output the version number
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
* the linting operation to short circuit and not report any failures.
* @property {string[]} _ Positional filenames or patterns
* @property {boolean} [stats] Report additional statistics
*/
//------------------------------------------------------------------------------
@ -101,6 +104,16 @@ module.exports = function(usingFlatConfig) {
};
}
let inspectConfigFlag;
if (usingFlatConfig) {
inspectConfigFlag = {
option: "inspect-config",
type: "Boolean",
description: "Open the config inspector with the current configuration"
};
}
let extFlag;
if (!usingFlatConfig) {
@ -141,6 +154,17 @@ module.exports = function(usingFlatConfig) {
};
}
let statsFlag;
if (usingFlatConfig) {
statsFlag = {
option: "stats",
type: "Boolean",
default: "false",
description: "Add statistics to the lint report"
};
}
let warnIgnoredFlag;
if (usingFlatConfig) {
@ -171,6 +195,7 @@ module.exports = function(usingFlatConfig) {
? "Use this configuration instead of eslint.config.js, eslint.config.mjs, or eslint.config.cjs"
: "Use this configuration, overriding .eslintrc.* config options if present"
},
inspectConfigFlag,
envFlag,
extFlag,
{
@ -236,7 +261,7 @@ module.exports = function(usingFlatConfig) {
{
option: "ignore-pattern",
type: "[String]",
description: "Pattern of files to ignore (in addition to those in .eslintignore)",
description: `Patterns of files to ignore${usingFlatConfig ? "" : " (in addition to those in .eslintignore)"}`,
concatRepeatedArrays: [true, {
oneValuePerFlag: true
}]
@ -370,6 +395,12 @@ module.exports = function(usingFlatConfig) {
description: "Exit with exit code 2 in case of fatal error"
},
warnIgnoredFlag,
{
option: "pass-on-no-patterns",
type: "Boolean",
default: false,
description: "Exit with exit code 0 in case no file patterns are passed"
},
{
option: "debug",
type: "Boolean",
@ -392,7 +423,8 @@ module.exports = function(usingFlatConfig) {
option: "print-config",
type: "path::String",
description: "Print the configuration for the given file"
}
},
statsFlag
].filter(value => !!value)
});
};

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,7 @@
"use strict";
const RuleTester = require("./rule-tester");
module.exports = {
RuleTester: require("./rule-tester")
RuleTester
};

File diff suppressed because it is too large Load diff

View file

@ -74,7 +74,7 @@ module.exports = {
function normalizeOptionValue(option) {
let consistent = false;
let multiline = false;
let minItems = 0;
let minItems;
if (option) {
if (option === "consistent") {

View file

@ -199,7 +199,7 @@ module.exports = {
: sourceCode.getLastToken(node),
penultimate = sourceCode.getTokenBefore(last),
firstElement = node.elements[0],
lastElement = node.elements[node.elements.length - 1];
lastElement = node.elements.at(-1);
const openingBracketMustBeSpaced =
options.objectsInArraysException && isObjectType(firstElement) ||

View file

@ -79,7 +79,7 @@ module.exports = {
}
// Defines a predicate to check whether or not a given reference is outside of valid scope.
const scopeRange = stack[stack.length - 1];
const scopeRange = stack.at(-1);
/**
* Check if a reference is out of scope

View file

@ -147,7 +147,7 @@ module.exports = {
if (closestBlock.type === "BlockStatement") {
// find the last item in the block
const lastItem = closestBlock.body[closestBlock.body.length - 1];
const lastItem = closestBlock.body.at(-1);
// if the callback is the last thing in a block that might be ok
if (isCallbackExpression(node, lastItem)) {
@ -168,7 +168,7 @@ module.exports = {
if (lastItem.type === "ReturnStatement") {
// but only if the callback is immediately before
if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) {
if (isCallbackExpression(node, closestBlock.body.at(-2))) {
return;
}
}

View file

@ -47,11 +47,9 @@ module.exports = {
},
allow: {
type: "array",
items: [
{
type: "string"
}
],
items: {
type: "string"
},
minItems: 0,
uniqueItems: true
}

View file

@ -8,7 +8,6 @@
// Requirements
//------------------------------------------------------------------------------
const LETTER_PATTERN = require("./utils/patterns/letters");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
@ -17,7 +16,8 @@ const astUtils = require("./utils/ast-utils");
const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
WHITESPACE = /\s/gu,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u, // TODO: Combine w/ max-len pattern?
LETTER_PATTERN = /\p{L}/u;
/*
* Base schema body for defining the basic capitalization rule, ignorePattern,
@ -233,7 +233,8 @@ module.exports = {
return true;
}
const firstWordChar = commentWordCharsOnly[0];
// Get the first Unicode character (1 or 2 code units).
const [firstWordChar] = commentWordCharsOnly;
if (!LETTER_PATTERN.test(firstWordChar)) {
return true;
@ -273,12 +274,14 @@ module.exports = {
messageId,
fix(fixer) {
const match = comment.value.match(LETTER_PATTERN);
const char = match[0];
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
const charIndex = comment.range[0] + match.index + 2;
return fixer.replaceTextRange(
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
[comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
[charIndex, charIndex + char.length],
capitalize === "always" ? char.toLocaleUpperCase() : char.toLocaleLowerCase()
);
}
});

View file

@ -154,7 +154,7 @@ module.exports = {
* @returns {any} The last element
*/
function last(array) {
return array[array.length - 1];
return array.at(-1);
}
switch (node.type) {

View file

@ -66,7 +66,7 @@ module.exports = {
NewExpression: true
};
if (context.options.length === 2 && Object.prototype.hasOwnProperty.call(context.options[1], "exceptions")) {
if (context.options.length === 2 && Object.hasOwn(context.options[1], "exceptions")) {
const keys = Object.keys(context.options[1].exceptions);
for (let i = 0; i < keys.length; i++) {
@ -218,7 +218,7 @@ module.exports = {
previousItemToken = tokenAfterItem
? sourceCode.getTokenBefore(tokenAfterItem)
: sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1];
: sourceCode.ast.tokens.at(-1);
} else {
previousItemToken = currentItemToken;
}

View file

@ -64,7 +64,7 @@ module.exports = {
if (
typeof option === "object" &&
(Object.prototype.hasOwnProperty.call(option, "maximum") || Object.prototype.hasOwnProperty.call(option, "max"))
(Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max"))
) {
THRESHOLD = option.maximum || option.max;
} else if (typeof option === "number") {
@ -109,6 +109,7 @@ module.exports = {
IfStatement: increaseComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity,
AssignmentPattern: increaseComplexity,
// Avoid `default`
"SwitchCase[test]": increaseComplexity,
@ -120,6 +121,18 @@ module.exports = {
}
},
MemberExpression(node) {
if (node.optional === true) {
increaseComplexity();
}
},
CallExpression(node) {
if (node.optional === true) {
increaseComplexity();
}
},
onCodePathEnd(codePath, node) {
const complexity = complexities.pop();

View file

@ -9,22 +9,6 @@
// Helpers
//------------------------------------------------------------------------------
/**
* Checks all segments in a set and returns true if any are reachable.
* @param {Set<CodePathSegment>} segments The segments to check.
* @returns {boolean} True if any segment is reachable; false otherwise.
*/
function isAnySegmentReachable(segments) {
for (const segment of segments) {
if (segment.reachable) {
return true;
}
}
return false;
}
/**
* Checks whether or not a given node is a constructor.
* @param {ASTNode} node A node to check. This node type is one of
@ -109,7 +93,7 @@ function isPossibleConstructor(node) {
);
case "SequenceExpression": {
const lastExpression = node.expressions[node.expressions.length - 1];
const lastExpression = node.expressions.at(-1);
return isPossibleConstructor(lastExpression);
}
@ -119,6 +103,30 @@ function isPossibleConstructor(node) {
}
}
/**
* A class to store information about a code path segment.
*/
class SegmentInfo {
/**
* Indicates if super() is called in all code paths.
* @type {boolean}
*/
calledInEveryPaths = false;
/**
* Indicates if super() is called in any code paths.
* @type {boolean}
*/
calledInSomePaths = false;
/**
* The nodes which have been validated and don't need to be reconsidered.
* @type {ASTNode[]}
*/
validNodes = [];
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -141,8 +149,7 @@ module.exports = {
missingAll: "Expected to call 'super()'.",
duplicate: "Unexpected duplicate 'super()'.",
badSuper: "Unexpected 'super()' because 'super' is not a constructor.",
unexpected: "Unexpected 'super()'."
badSuper: "Unexpected 'super()' because 'super' is not a constructor."
}
},
@ -159,14 +166,10 @@ module.exports = {
*/
let funcInfo = null;
/*
* {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
* Information for each code path segment.
* - calledInSomePaths: A flag of be called `super()` in some code paths.
* - calledInEveryPaths: A flag of be called `super()` in all code paths.
* - validNodes:
/**
* @type {Record<string, SegmentInfo>}
*/
let segInfoMap = Object.create(null);
const segInfoMap = Object.create(null);
/**
* Gets the flag which shows `super()` is called in some paths.
@ -177,23 +180,21 @@ module.exports = {
return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
}
/**
* Determines if a segment has been seen in the traversal.
* @param {CodePathSegment} segment A code path segment to check.
* @returns {boolean} `true` if the segment has been seen.
*/
function hasSegmentBeenSeen(segment) {
return !!segInfoMap[segment.id];
}
/**
* Gets the flag which shows `super()` is called in all paths.
* @param {CodePathSegment} segment A code path segment to get.
* @returns {boolean} The flag which shows `super()` is called in all paths.
*/
function isCalledInEveryPath(segment) {
/*
* If specific segment is the looped segment of the current segment,
* skip the segment.
* If not skipped, this never becomes true after a loop.
*/
if (segment.nextSegments.length === 1 &&
segment.nextSegments[0].isLoopedPrevSegment(segment)
) {
return true;
}
return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
}
@ -250,9 +251,9 @@ module.exports = {
}
// Reports if `super()` lacked.
const segments = codePath.returnedSegments;
const calledInEveryPaths = segments.every(isCalledInEveryPath);
const calledInSomePaths = segments.some(isCalledInSomePath);
const returnedSegments = codePath.returnedSegments;
const calledInEveryPaths = returnedSegments.every(isCalledInEveryPath);
const calledInSomePaths = returnedSegments.some(isCalledInSomePath);
if (!calledInEveryPaths) {
context.report({
@ -267,29 +268,37 @@ module.exports = {
/**
* Initialize information of a given code path segment.
* @param {CodePathSegment} segment A code path segment to initialize.
* @param {CodePathSegment} node Node that starts the segment.
* @returns {void}
*/
onCodePathSegmentStart(segment) {
onCodePathSegmentStart(segment, node) {
funcInfo.currentSegments.add(segment);
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Initialize info.
const info = segInfoMap[segment.id] = {
calledInSomePaths: false,
calledInEveryPaths: false,
validNodes: []
};
const info = segInfoMap[segment.id] = new SegmentInfo();
const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
// When there are previous segments, aggregates these.
const prevSegments = segment.prevSegments;
if (seenPrevSegments.length > 0) {
info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
}
if (prevSegments.length > 0) {
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
/*
* ForStatement > *.update segments are a special case as they are created in advance,
* without seen previous segments. Since they logically don't affect `calledInEveryPaths`
* calculations, and they can never be a lone previous segment of another one, we'll set
* their `calledInEveryPaths` to `true` to effectively ignore them in those calculations.
* .
*/
if (node.parent && node.parent.type === "ForStatement" && node.parent.update === node) {
info.calledInEveryPaths = true;
}
},
@ -316,25 +325,30 @@ module.exports = {
* @returns {void}
*/
onCodePathSegmentLoop(fromSegment, toSegment) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
// Update information inside of the loop.
const isRealLoop = toSegment.prevSegments.length >= 2;
funcInfo.codePath.traverseSegments(
{ first: toSegment, last: fromSegment },
segment => {
(segment, controller) => {
const info = segInfoMap[segment.id];
const prevSegments = segment.prevSegments;
// Updates flags.
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
// skip segments after the loop
if (!info) {
controller.skip();
return;
}
const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
const calledInSomePreviousPaths = seenPrevSegments.some(isCalledInSomePath);
const calledInEveryPreviousPaths = seenPrevSegments.every(isCalledInEveryPath);
info.calledInSomePaths ||= calledInSomePreviousPaths;
info.calledInEveryPaths ||= calledInEveryPreviousPaths;
// If flags become true anew, reports the valid nodes.
if (info.calledInSomePaths || isRealLoop) {
if (calledInSomePreviousPaths) {
const nodes = info.validNodes;
info.validNodes = [];
@ -358,7 +372,7 @@ module.exports = {
* @returns {void}
*/
"CallExpression:exit"(node) {
if (!(funcInfo && funcInfo.isConstructor)) {
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
@ -368,41 +382,34 @@ module.exports = {
}
// Reports if needed.
if (funcInfo.hasExtends) {
const segments = funcInfo.currentSegments;
let duplicate = false;
let info = null;
const segments = funcInfo.currentSegments;
let duplicate = false;
let info = null;
for (const segment of segments) {
for (const segment of segments) {
if (segment.reachable) {
info = segInfoMap[segment.id];
if (segment.reachable) {
info = segInfoMap[segment.id];
duplicate = duplicate || info.calledInSomePaths;
info.calledInSomePaths = info.calledInEveryPaths = true;
}
duplicate = duplicate || info.calledInSomePaths;
info.calledInSomePaths = info.calledInEveryPaths = true;
}
}
if (info) {
if (duplicate) {
context.report({
messageId: "duplicate",
node
});
} else if (!funcInfo.superIsConstructor) {
context.report({
messageId: "badSuper",
node
});
} else {
info.validNodes.push(node);
}
if (info) {
if (duplicate) {
context.report({
messageId: "duplicate",
node
});
} else if (!funcInfo.superIsConstructor) {
context.report({
messageId: "badSuper",
node
});
} else {
info.validNodes.push(node);
}
} else if (isAnySegmentReachable(funcInfo.currentSegments)) {
context.report({
messageId: "unexpected",
node
});
}
},
@ -412,7 +419,7 @@ module.exports = {
* @returns {void}
*/
ReturnStatement(node) {
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
return;
}
@ -432,14 +439,6 @@ module.exports = {
info.calledInSomePaths = info.calledInEveryPaths = true;
}
}
},
/**
* Resets state.
* @returns {void}
*/
"Program:exit"() {
segInfoMap = Object.create(null);
}
};
}

View file

@ -54,7 +54,7 @@ module.exports = {
* @returns {any} Last element
*/
function last(collection) {
return collection[collection.length - 1];
return collection.at(-1);
}
//--------------------------------------------------------------------------

View file

@ -45,7 +45,7 @@ module.exports = {
Program: function checkBadEOF(node) {
const sourceCode = context.sourceCode,
src = sourceCode.getText(),
lastLine = sourceCode.lines[sourceCode.lines.length - 1],
lastLine = sourceCode.lines.at(-1),
location = {
column: lastLine.length,
line: sourceCode.lines.length
@ -89,7 +89,7 @@ module.exports = {
});
} else if (mode === "never" && endsWithNewline) {
const secondLastLine = sourceCode.lines[sourceCode.lines.length - 2];
const secondLastLine = sourceCode.lines.at(-2);
// File is newline-terminated, but shouldn't be
context.report({

View file

@ -29,6 +29,15 @@ module.exports = {
allowArrowFunctions: {
type: "boolean",
default: false
},
overrides: {
type: "object",
properties: {
namedExports: {
enum: ["declaration", "expression", "ignore"]
}
},
additionalProperties: false
}
},
additionalProperties: false
@ -46,13 +55,22 @@ module.exports = {
const style = context.options[0],
allowArrowFunctions = context.options[1] && context.options[1].allowArrowFunctions,
enforceDeclarations = (style === "declaration"),
exportFunctionStyle = context.options[1] && context.options[1].overrides && context.options[1].overrides.namedExports,
stack = [];
const nodesToCheck = {
FunctionDeclaration(node) {
stack.push(false);
if (!enforceDeclarations && node.parent.type !== "ExportDefaultDeclaration") {
if (
!enforceDeclarations &&
node.parent.type !== "ExportDefaultDeclaration" &&
(typeof exportFunctionStyle === "undefined" || node.parent.type !== "ExportNamedDeclaration")
) {
context.report({ node, messageId: "expression" });
}
if (node.parent.type === "ExportNamedDeclaration" && exportFunctionStyle === "expression") {
context.report({ node, messageId: "expression" });
}
},
@ -63,7 +81,18 @@ module.exports = {
FunctionExpression(node) {
stack.push(false);
if (enforceDeclarations && node.parent.type === "VariableDeclarator") {
if (
enforceDeclarations &&
node.parent.type === "VariableDeclarator" &&
(typeof exportFunctionStyle === "undefined" || node.parent.parent.parent.type !== "ExportNamedDeclaration")
) {
context.report({ node: node.parent, messageId: "declaration" });
}
if (
node.parent.type === "VariableDeclarator" && node.parent.parent.parent.type === "ExportNamedDeclaration" &&
exportFunctionStyle === "declaration"
) {
context.report({ node: node.parent, messageId: "declaration" });
}
},
@ -86,8 +115,17 @@ module.exports = {
nodesToCheck["ArrowFunctionExpression:exit"] = function(node) {
const hasThisExpr = stack.pop();
if (enforceDeclarations && !hasThisExpr && node.parent.type === "VariableDeclarator") {
context.report({ node: node.parent, messageId: "declaration" });
if (!hasThisExpr && node.parent.type === "VariableDeclarator") {
if (
enforceDeclarations &&
(typeof exportFunctionStyle === "undefined" || node.parent.parent.parent.type !== "ExportNamedDeclaration")
) {
context.report({ node: node.parent, messageId: "declaration" });
}
if (node.parent.parent.parent.type === "ExportNamedDeclaration" && exportFunctionStyle === "declaration") {
context.report({ node: node.parent, messageId: "declaration" });
}
}
};
}

Some files were not shown because too many files have changed in this diff Show more