diff --git a/doc/api/modules.md b/doc/api/modules.md index 92897facf4446e..a401d71f871cef 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -1126,7 +1126,7 @@ exports = { hello: false }; // Not exported, only available in the module When the `module.exports` property is being completely replaced by a new object, it is common to also reassign `exports`: - + ```js module.exports = exports = function Constructor() { diff --git a/eslint.config.mjs b/eslint.config.mjs index 9eaa067eba75d3..d2861025dcdd88 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -174,9 +174,6 @@ export default [ 'dot-notation': 'error', 'eqeqeq': ['error', 'smart'], - // TODO: make this rule consider primordials - 'func-name-matching': ['error', { considerPropertyDescriptor: true }], - 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], 'no-constant-condition': ['error', { checkLoops: false }], 'no-constructor-return': 'error', @@ -343,6 +340,7 @@ export default [ 'node-core/no-duplicate-requires': 'error', 'node-core/prefer-proto': 'error', 'node-core/prefer-optional-chaining': 'error', + 'node-core/func-name-matching': ['error', { considerPropertyDescriptor: true }], }, }, // #endregion diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index 4571922ed5a0e9..20a6f721cc7e71 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -721,7 +721,7 @@ function nextHookFactory(current, meta, { validateArgs, validateOutput }) { if (next) { nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput }); } else { - // eslint-disable-next-line func-name-matching + // eslint-disable-next-line node-core/func-name-matching nextNextHook = function chainAdvancedTooFar() { throw new ERR_INTERNAL_ASSERTION( `ESM custom loader '${hookName}' advanced beyond the end of the chain.`, diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 0a8e4c9e5d3af5..f3db89c23b170a 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -129,7 +129,7 @@ function loadCJSModule(module, source, url, filename, isMain) { const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const __dirname = dirname(filename); - // eslint-disable-next-line func-name-matching,func-style + // eslint-disable-next-line node-core/func-name-matching,func-style const requireFn = function require(specifier) { let importAttributes = kEmptyObject; if (!StringPrototypeStartsWith(specifier, 'node:') && !BuiltinModule.normalizeRequirableId(specifier)) { diff --git a/test/parallel/test-eslint-func-name-matching.js b/test/parallel/test-eslint-func-name-matching.js new file mode 100644 index 00000000000000..b5860e14812d34 --- /dev/null +++ b/test/parallel/test-eslint-func-name-matching.js @@ -0,0 +1,1128 @@ +// Copyright OpenJS Foundation and other contributors, + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// @fileoverview Tests for func-name-matching rule. +// @author Annie Zhang + +'use strict'; +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} +common.skipIfEslintMissing(); + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require('../../tools/eslint-rules/func-name-matching'); +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +ruleTester.run('func-name-matching', rule, { + valid: [ + 'var foo;', + 'var foo = function foo() {};', + { code: 'var foo = function foo() {};', options: ['always'] }, + { code: 'var foo = function bar() {};', options: ['never'] }, + 'var foo = function() {}', + { code: 'var foo = () => {}', languageOptions: { ecmaVersion: 6 } }, + 'foo = function foo() {};', + { code: 'foo = function foo() {};', options: ['always'] }, + { code: 'foo = function bar() {};', options: ['never'] }, + { + code: 'foo &&= function foo() {};', + languageOptions: { ecmaVersion: 2021 }, + }, + { + code: 'obj.foo ||= function foo() {};', + languageOptions: { ecmaVersion: 2021 }, + }, + { + code: 'obj[\'foo\'] ??= function foo() {};', + languageOptions: { ecmaVersion: 2021 }, + }, + 'obj.foo = function foo() {};', + { code: 'obj.foo = function foo() {};', options: ['always'] }, + { code: 'obj.foo = function bar() {};', options: ['never'] }, + 'obj.foo = function() {};', + { code: 'obj.foo = function() {};', options: ['always'] }, + { code: 'obj.foo = function() {};', options: ['never'] }, + 'obj.bar.foo = function foo() {};', + { code: 'obj.bar.foo = function foo() {};', options: ['always'] }, + { code: 'obj.bar.foo = function baz() {};', options: ['never'] }, + 'obj[\'foo\'] = function foo() {};', + { code: 'obj[\'foo\'] = function foo() {};', options: ['always'] }, + { code: 'obj[\'foo\'] = function bar() {};', options: ['never'] }, + 'obj[\'foo//bar\'] = function foo() {};', + { code: 'obj[\'foo//bar\'] = function foo() {};', options: ['always'] }, + { code: 'obj[\'foo//bar\'] = function foo() {};', options: ['never'] }, + 'obj[foo] = function bar() {};', + { code: 'obj[foo] = function bar() {};', options: ['always'] }, + { code: 'obj[foo] = function bar() {};', options: ['never'] }, + 'var obj = {foo: function foo() {}};', + { code: 'var obj = {foo: function foo() {}};', options: ['always'] }, + { code: 'var obj = {foo: function bar() {}};', options: ['never'] }, + 'var obj = {\'foo\': function foo() {}};', + { code: 'var obj = {\'foo\': function foo() {}};', options: ['always'] }, + { code: 'var obj = {\'foo\': function bar() {}};', options: ['never'] }, + 'var obj = {\'foo//bar\': function foo() {}};', + { + code: 'var obj = {\'foo//bar\': function foo() {}};', + options: ['always'], + }, + { + code: 'var obj = {\'foo//bar\': function foo() {}};', + options: ['never'], + }, + 'var obj = {foo: function() {}};', + { code: 'var obj = {foo: function() {}};', options: ['always'] }, + { code: 'var obj = {foo: function() {}};', options: ['never'] }, + { + code: 'var obj = {[foo]: function bar() {}} ', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'var obj = {[\'x\' + 2]: function bar(){}};', + languageOptions: { ecmaVersion: 6 }, + }, + 'obj[\'x\' + 2] = function bar(){};', + { + code: 'var [ bar ] = [ function bar(){} ];', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'function a(foo = function bar() {}) {}', + languageOptions: { ecmaVersion: 6 }, + }, + 'module.exports = function foo(name) {};', + 'module[\'exports\'] = function foo(name) {};', + { + code: 'module.exports = function foo(name) {};', + options: [{ includeCommonJSModuleExports: false }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'module.exports = function foo(name) {};', + options: ['always', { includeCommonJSModuleExports: false }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'module.exports = function foo(name) {};', + options: ['never', { includeCommonJSModuleExports: false }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'module[\'exports\'] = function foo(name) {};', + options: [{ includeCommonJSModuleExports: false }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'module[\'exports\'] = function foo(name) {};', + options: ['always', { includeCommonJSModuleExports: false }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'module[\'exports\'] = function foo(name) {};', + options: ['never', { includeCommonJSModuleExports: false }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[\'foo\']: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[\'foo\']: function foo() {}})', + options: ['always'], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[\'foo\']: function bar() {}})', + options: ['never'], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[\'❤\']: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[foo]: function bar() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[null]: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[1]: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[true]: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[`x`]: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[/abc/]: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[[1, 2, 3]]: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({[{x: 1}]: function foo() {}})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '[] = function foo() {}', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({} = function foo() {})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '[a] = function foo() {}', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({a} = function foo() {})', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'var [] = function foo() {}', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'var {} = function foo() {}', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'var [a] = function foo() {}', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'var {a} = function foo() {}', + languageOptions: { ecmaVersion: 6 }, + }, + { + code: '({ value: function value() {} })', + options: [{ considerPropertyDescriptor: true }], + }, + { + code: 'obj.foo = function foo() {};', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'obj.bar.foo = function foo() {};', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'var obj = {foo: function foo() {}};', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'var obj = {foo: function() {}};', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'var obj = { value: function value() {} }', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'Object.defineProperty(foo, \'bar\', { value: function bar() {} })', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'Object.defineProperties(foo, { bar: { value: function bar() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'Object.create(proto, { bar: { value: function bar() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'Object.defineProperty(foo, \'b\' + \'ar\', { value: function bar() {} })', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'Object.defineProperties(foo, { [\'bar\']: { value: function bar() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'Object.create(proto, { [\'bar\']: { value: function bar() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'Object.defineProperty(foo, \'bar\', { value() {} })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'Object.defineProperties(foo, { bar: { value() {} } })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'Object.create(proto, { bar: { value() {} } })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'Reflect.defineProperty(foo, \'bar\', { value: function bar() {} })', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'Reflect.defineProperty(foo, \'b\' + \'ar\', { value: function baz() {} })', + options: ['always', { considerPropertyDescriptor: true }], + }, + { + code: 'Reflect.defineProperty(foo, \'bar\', { value() {} })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: 'foo({ value: function value() {} })', + options: ['always', { considerPropertyDescriptor: true }], + }, + + // Class fields, private names are ignored + { + code: 'class C { x = function () {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { x = function () {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { \'x\' = function () {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { \'x\' = function () {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x = function () {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x = function () {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [x] = function () {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [x] = function () {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [\'x\'] = function () {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [\'x\'] = function () {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { x = function x() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { x = function y() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { \'x\' = function x() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { \'x\' = function y() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x = function x() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x = function x() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x = function y() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x = function y() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [x] = function x() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [x] = function x() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [x] = function y() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [x] = function y() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [\'x\'] = function x() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [\'x\'] = function y() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { \'xy \' = function foo() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { \'xy \' = function xy() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [\'xy \'] = function foo() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [\'xy \'] = function xy() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { 1 = function x0() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { 1 = function x1() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [1] = function x0() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [1] = function x1() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [f()] = function g() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { [f()] = function f() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { static x = function x() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { static x = function y() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { x = (function y() {})(); }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { x = (function x() {})(); }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: '(class { x = function x() {}; })', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: '(class { x = function y() {}; })', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { this.#x = function x() {}; } }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { this.#x = function x() {}; } }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { this.#x = function y() {}; } }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { this.#x = function y() {}; } }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { a.b.#x = function x() {}; } }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { a.b.#x = function x() {}; } }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { a.b.#x = function y() {}; } }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'class C { #x; foo() { a.b.#x = function y() {}; } }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: 'var obj = { \'\\u1885\': function foo() {} };', // Not a valid identifier in es5 + languageOptions: { + ecmaVersion: 5, + sourceType: 'script', + }, + }, + ], + invalid: [ + { + code: 'let foo = function bar() {};', + options: ['always'], + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchVariable', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'let foo = function bar() {};', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchVariable', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'foo = function bar() {};', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchVariable', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'foo &&= function bar() {};', + languageOptions: { ecmaVersion: 2021 }, + errors: [ + { + messageId: 'matchVariable', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'obj.foo ||= function bar() {};', + languageOptions: { ecmaVersion: 2021 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'obj[\'foo\'] ??= function bar() {};', + languageOptions: { ecmaVersion: 2021 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'obj.foo = function bar() {};', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'obj.bar.foo = function bar() {};', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'obj[\'foo\'] = function bar() {};', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'let obj = {foo: function bar() {}};', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'let obj = {\'foo\': function bar() {}};', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: '({[\'foo\']: function bar() {}})', + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'module.exports = function foo(name) {};', + options: [{ includeCommonJSModuleExports: true }], + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'foo', name: 'exports' }, + }, + ], + }, + { + code: 'module.exports = function foo(name) {};', + options: ['always', { includeCommonJSModuleExports: true }], + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'foo', name: 'exports' }, + }, + ], + }, + { + code: 'module.exports = function exports(name) {};', + options: ['never', { includeCommonJSModuleExports: true }], + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'exports', name: 'exports' }, + }, + ], + }, + { + code: 'module[\'exports\'] = function foo(name) {};', + options: [{ includeCommonJSModuleExports: true }], + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'foo', name: 'exports' }, + }, + ], + }, + { + code: 'module[\'exports\'] = function foo(name) {};', + options: ['always', { includeCommonJSModuleExports: true }], + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'foo', name: 'exports' }, + }, + ], + }, + { + code: 'module[\'exports\'] = function exports(name) {};', + options: ['never', { includeCommonJSModuleExports: true }], + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'exports', name: 'exports' }, + }, + ], + }, + { + code: 'var foo = function foo(name) {};', + options: ['never'], + errors: [ + { + messageId: 'notMatchVariable', + data: { funcName: 'foo', name: 'foo' }, + }, + ], + }, + { + code: 'obj.foo = function foo(name) {};', + options: ['never'], + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'foo', name: 'foo' }, + }, + ], + }, + { + code: 'Object.defineProperty(foo, \'bar\', { value: function baz() {} })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'Object.defineProperties(foo, { bar: { value: function baz() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'Object.create(proto, { bar: { value: function baz() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'var obj = { value: function foo(name) {} }', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'foo', name: 'value' }, + }, + ], + }, + { + code: 'Object.defineProperty(foo, \'bar\', { value: function bar() {} })', + options: ['never', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + { + code: 'Object.defineProperties(foo, { bar: { value: function bar() {} } })', + options: ['never', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + { + code: 'Object.create(proto, { bar: { value: function bar() {} } })', + options: ['never', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + { + code: 'Reflect.defineProperty(foo, \'bar\', { value: function baz() {} })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'Reflect.defineProperty(foo, \'bar\', { value: function bar() {} })', + options: ['never', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + // Tests for Node's primordials (ObjectDefineProperty, ObjectDefineProperties, ReflectDefineProperty) + { + code: 'ObjectDefineProperty(foo, \'bar\', { value: function baz() {} })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'ReflectDefineProperty(foo, \'bar\', { value: function baz() {} })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'ObjectDefineProperties(foo, { bar: { value: function baz() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'foo({ value: function bar() {} })', + options: ['always', { considerPropertyDescriptor: true }], + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'value' }, + }, + ], + }, + + // Optional chaining + { + code: '(obj?.aaa).foo = function bar() {};', + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'bar', name: 'foo' }, + }, + ], + }, + { + code: 'Object?.defineProperty(foo, \'bar\', { value: function baz() {} })', + options: ['always', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: '(Object?.defineProperty)(foo, \'bar\', { value: function baz() {} })', + options: ['always', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'Object?.defineProperty(foo, \'bar\', { value: function bar() {} })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + { + code: '(Object?.defineProperty)(foo, \'bar\', { value: function bar() {} })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + { + code: 'Object?.defineProperties(foo, { bar: { value: function baz() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: '(Object?.defineProperties)(foo, { bar: { value: function baz() {} } })', + options: ['always', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'baz', name: 'bar' }, + }, + ], + }, + { + code: 'Object?.defineProperties(foo, { bar: { value: function bar() {} } })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + { + code: '(Object?.defineProperties)(foo, { bar: { value: function bar() {} } })', + options: ['never', { considerPropertyDescriptor: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'bar', name: 'bar' }, + }, + ], + }, + + // class fields + { + code: 'class C { x = function y() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'y', name: 'x' }, + }, + ], + }, + { + code: 'class C { x = function x() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'x', name: 'x' }, + }, + ], + }, + { + code: 'class C { \'x\' = function y() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'y', name: 'x' }, + }, + ], + }, + { + code: 'class C { \'x\' = function x() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'x', name: 'x' }, + }, + ], + }, + { + code: 'class C { [\'x\'] = function y() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'y', name: 'x' }, + }, + ], + }, + { + code: 'class C { [\'x\'] = function x() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'x', name: 'x' }, + }, + ], + }, + { + code: 'class C { static x = function y() {}; }', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'y', name: 'x' }, + }, + ], + }, + { + code: 'class C { static x = function x() {}; }', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'x', name: 'x' }, + }, + ], + }, + { + code: '(class { x = function y() {}; })', + options: ['always'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'y', name: 'x' }, + }, + ], + }, + { + code: '(class { x = function x() {}; })', + options: ['never'], + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: 'notMatchProperty', + data: { funcName: 'x', name: 'x' }, + }, + ], + }, + { + code: 'var obj = { \'\\u1885\': function foo() {} };', // Valid identifier in es2015 + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'matchProperty', + data: { funcName: 'foo', name: '\u1885' }, + }, + ], + }, + ], +}); diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 2dc263443481a0..c26e5b261a9a73 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -107,7 +107,7 @@ assert.strictEqual(util.inspect({}), '{}'); assert.strictEqual(util.inspect({ a: 1 }), '{ a: 1 }'); assert.strictEqual(util.inspect({ a: function() {} }), '{ a: [Function: a] }'); assert.strictEqual(util.inspect({ a: () => {} }), '{ a: [Function: a] }'); -// eslint-disable-next-line func-name-matching +// eslint-disable-next-line node-core/func-name-matching assert.strictEqual(util.inspect({ a: async function abc() {} }), '{ a: [AsyncFunction: abc] }'); assert.strictEqual(util.inspect({ a: async () => {} }), diff --git a/tools/eslint-rules/func-name-matching.js b/tools/eslint-rules/func-name-matching.js new file mode 100644 index 00000000000000..3d49408fbec277 --- /dev/null +++ b/tools/eslint-rules/func-name-matching.js @@ -0,0 +1,365 @@ +// Copyright OpenJS Foundation and other contributors, + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// @fileoverview Rule to require function names to match the name of the variable or property to which they +// are assigned. +// @author Annie Zhang +// @author Pavel Strashkin + +'use strict'; + +//-------------------------------------------------------------------------- +// Requirements +//-------------------------------------------------------------------------- + +const astUtils = require('../eslint/node_modules/eslint/lib/rules/utils/ast-utils'); +const esutils = require('../eslint/node_modules/esutils'); + +//-------------------------------------------------------------------------- +// Helpers +//-------------------------------------------------------------------------- + +/** + * Determines if a pattern is `module.exports` or `module['exports']` + * @param {ASTNode} pattern The left side of the AssignmentExpression + * @returns {boolean} True if the pattern is `module.exports` or `module['exports']` + */ +function isModuleExports(pattern) { + if ( + pattern.type === 'MemberExpression' && + pattern.object.type === 'Identifier' && + pattern.object.name === 'module' + ) { + // module.exports + if ( + pattern.property.type === 'Identifier' && + pattern.property.name === 'exports' + ) { + return true; + } + + // module['exports'] + if ( + pattern.property.type === 'Literal' && + pattern.property.value === 'exports' + ) { + return true; + } + } + return false; +} + +/** + * Determines if a string name is a valid identifier + * @param {string} name The string to be checked + * @param {number} ecmaVersion The ECMAScript version if specified in the parserOptions config + * @returns {boolean} True if the string is a valid identifier + */ +function isIdentifier(name, ecmaVersion) { + if (ecmaVersion >= 2015) { + return esutils.keyword.isIdentifierES6(name); + } + return esutils.keyword.isIdentifierES5(name); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const alwaysOrNever = { enum: ['always', 'never'] }; +const optionsObject = { + type: 'object', + properties: { + considerPropertyDescriptor: { + type: 'boolean', + }, + includeCommonJSModuleExports: { + type: 'boolean', + }, + }, + additionalProperties: false, +}; + +/** @type {import('../types').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + + docs: { + description: + 'Require function names to match the name of the variable or property to which they are assigned', + recommended: false, + frozen: true, + url: 'https://eslint.org/docs/latest/rules/func-name-matching', + }, + + schema: { + anyOf: [ + { + type: 'array', + additionalItems: false, + items: [alwaysOrNever, optionsObject], + }, + { + type: 'array', + additionalItems: false, + items: [optionsObject], + }, + ], + }, + + messages: { + matchProperty: + 'Function name `{{funcName}}` should match property name `{{name}}`.', + matchVariable: + 'Function name `{{funcName}}` should match variable name `{{name}}`.', + notMatchProperty: + 'Function name `{{funcName}}` should not match property name `{{name}}`.', + notMatchVariable: + 'Function name `{{funcName}}` should not match variable name `{{name}}`.', + }, + }, + + create(context) { + const options = + (typeof context.options[0] === 'object' ? + context.options[0] : context.options[1]) || {}; + const nameMatches = + typeof context.options[0] === 'string' ? + context.options[0] : 'always'; + const considerPropertyDescriptor = options.considerPropertyDescriptor; + const includeModuleExports = options.includeCommonJSModuleExports; + const ecmaVersion = context.languageOptions.ecmaVersion; + + /** + * Check whether node is a certain CallExpression. + * @param {string} objName object name + * @param {string} funcName function name + * @param {ASTNode} node The node to check + * @returns {boolean} `true` if node matches CallExpression + */ + function isPropertyCall(objName, funcName, node) { + if (!node) { + return false; + } + return ( + node.type === 'CallExpression' && + astUtils.isSpecificMemberAccess(node.callee, objName, funcName) + ); + } + + function isIdentifierCall(expectedIdentifierName, node) { + if (!node) { + return false; + } + return node.type === 'CallExpression' && + astUtils.isSpecificId(node.callee, expectedIdentifierName); + } + + /** + * Compares identifiers based on the nameMatches option + * @param {string} x the first identifier + * @param {string} y the second identifier + * @returns {boolean} whether the two identifiers should warn. + */ + function shouldWarn(x, y) { + return ( + (nameMatches === 'always' && x !== y) || + (nameMatches === 'never' && x === y) + ); + } + + /** + * Reports + * @param {ASTNode} node The node to report + * @param {string} name The variable or property name + * @param {string} funcName The function name + * @param {boolean} isProp True if the reported node is a property assignment + * @returns {void} + */ + function report(node, name, funcName, isProp) { + let messageId; + + if (nameMatches === 'always' && isProp) { + messageId = 'matchProperty'; + } else if (nameMatches === 'always') { + messageId = 'matchVariable'; + } else if (isProp) { + messageId = 'notMatchProperty'; + } else { + messageId = 'notMatchVariable'; + } + context.report({ + node, + messageId, + data: { + name, + funcName, + }, + }); + } + + /** + * Determines whether a given node is a string literal + * @param {ASTNode} node The node to check + * @returns {boolean} `true` if the node is a string literal + */ + function isStringLiteral(node) { + return node.type === 'Literal' && typeof node.value === 'string'; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + VariableDeclarator(node) { + if ( + !node.init || + node.init.type !== 'FunctionExpression' || + node.id.type !== 'Identifier' + ) { + return; + } + if ( + node.init.id && + shouldWarn(node.id.name, node.init.id.name) + ) { + report(node, node.id.name, node.init.id.name, false); + } + }, + + AssignmentExpression(node) { + if ( + node.right.type !== 'FunctionExpression' || + (node.left.computed && + node.left.property.type !== 'Literal') || + (!includeModuleExports && isModuleExports(node.left)) || + (node.left.type !== 'Identifier' && + node.left.type !== 'MemberExpression') + ) { + return; + } + + const isProp = node.left.type === 'MemberExpression'; + const name = isProp ? astUtils.getStaticPropertyName(node.left) : node.left.name; + + if ( + node.right.id && + name && + isIdentifier(name) && + shouldWarn(name, node.right.id.name) + ) { + report(node, name, node.right.id.name, isProp); + } + }, + + 'Property, PropertyDefinition[value]'(node) { + if ( + !(node.value.type === 'FunctionExpression' && node.value.id) + ) { + return; + } + + if (node.key.type === 'Identifier' && !node.computed) { + const functionName = node.value.id.name; + let propertyName = node.key.name; + + if ( + considerPropertyDescriptor && + propertyName === 'value' && + node.parent.type === 'ObjectExpression' + ) { + if ( + isPropertyCall( + 'Object', + 'defineProperty', + node.parent.parent, + ) || + isPropertyCall( + 'Reflect', + 'defineProperty', + node.parent.parent, + ) || + isIdentifierCall('ObjectDefineProperty', node.parent.parent) || + isIdentifierCall('ReflectDefineProperty', node.parent.parent) + ) { + const property = node.parent.parent.arguments[1]; + + if ( + isStringLiteral(property) && + shouldWarn(property.value, functionName) + ) { + report( + node, + property.value, + functionName, + true, + ); + } + } else if ( + isPropertyCall( + 'Object', + 'defineProperties', + node.parent.parent.parent.parent, + ) || + isIdentifierCall('ObjectDefineProperties', node.parent.parent.parent.parent) + ) { + propertyName = node.parent.parent.key.name; + if ( + !node.parent.parent.computed && + shouldWarn(propertyName, functionName) + ) { + report(node, propertyName, functionName, true); + } + } else if ( + isPropertyCall( + 'Object', + 'create', + node.parent.parent.parent.parent, + ) + ) { + propertyName = node.parent.parent.key.name; + if ( + !node.parent.parent.computed && + shouldWarn(propertyName, functionName) + ) { + report(node, propertyName, functionName, true); + } + } else if (shouldWarn(propertyName, functionName)) { + report(node, propertyName, functionName, true); + } + } else if (shouldWarn(propertyName, functionName)) { + report(node, propertyName, functionName, true); + } + return; + } + + if ( + isStringLiteral(node.key) && + isIdentifier(node.key.value, ecmaVersion) && + shouldWarn(node.key.value, node.value.id.name) + ) { + report(node, node.key.value, node.value.id.name, true); + } + }, + }; + }, +};