diff --git a/.eslintrc.js b/.eslintrc.js index b0fe642f250416..b6dd9cbcfff3e8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,11 +38,11 @@ module.exports = { { files: ['Libraries/**/*.js'], rules: { - '@react-native-community/error-subclass-name': 2, '@react-native-community/platform-colors': 2, '@react-native/specs/react-native-modules': 2, 'lint/no-haste-imports': 2, 'lint/no-react-native-imports': 2, + 'lint/require-extends-error': 2, }, }, { diff --git a/packages/eslint-plugin-react-native-community/README.md b/packages/eslint-plugin-react-native-community/README.md index 6b04171a6a6dff..4512dff9efc2dd 100644 --- a/packages/eslint-plugin-react-native-community/README.md +++ b/packages/eslint-plugin-react-native-community/README.md @@ -22,12 +22,6 @@ Add to your eslint config (`.eslintrc`, or `eslintConfig` field in `package.json ## Rules -### `error-subclass-name` - -**NOTE:** This rule is primarily used for developing React Native itself and is not generally applicable to other projects. - -Enforces that error classes ( = classes with PascalCase names ending with `Error`) only extend other error classes, and that regular functions don't have names that could be mistaken for those of error classes. - ### `platform-colors` Enforces that calls to `PlatformColor` and `DynamicColorIOS` are statically analyzable to enable performance optimizations. diff --git a/packages/eslint-plugin-react-native-community/__tests__/error-subclass-name-test.js b/packages/eslint-plugin-react-native-community/__tests__/error-subclass-name-test.js deleted file mode 100644 index a45989203d7b34..00000000000000 --- a/packages/eslint-plugin-react-native-community/__tests__/error-subclass-name-test.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @oncall react_native - */ - -'use strict'; - -const ESLintTester = require('./eslint-tester.js'); - -const rule = require('../error-subclass-name.js'); - -const eslintTester = new ESLintTester(); - -const INVALID_SUPERCLASS_MESSAGE = - "'SomethingEndingWithError' must extend an error class (like 'Error') because its name is in PascalCase and ends with 'Error'."; -const INVALID_OWN_NAME_MESSAGE = - "'Foo' may not be the name of an error class. It should be in PascalCase and end with 'Error'."; -const MISSING_OWN_NAME_MESSAGE = - "An error class should have a PascalCase name ending with 'Error'."; -const INVALID_FUNCTION_NAME_MESSAGE = - "'SomethingEndingWithError' is a reserved name. PascalCase names ending with 'Error' are reserved for error classes and may not be used for regular functions. Either rename this function or convert it to a class that extends 'Error'."; - -eslintTester.run('../error-subclass-name', rule, { - valid: [ - 'class FooError extends Error {}', - '(class FooError extends Error {})', - 'class FooError extends SomethingEndingWithError {}', - '(class FooError extends SomethingEndingWithError {})', - 'function makeError() {}', - '(function () {})', - - // The following cases are currently allowed but could be disallowed in the - // future. This is technically an escape hatch. - 'class Foo extends SomeLibrary.FooError {}', - '(class extends SomeLibrary.FooError {})', - ], - invalid: [ - { - code: 'class SomethingEndingWithError {}', - errors: [{message: INVALID_SUPERCLASS_MESSAGE}], - }, - { - code: '(class SomethingEndingWithError {})', - errors: [{message: INVALID_SUPERCLASS_MESSAGE}], - }, - { - code: 'class Foo extends Error {}', - errors: [{message: INVALID_OWN_NAME_MESSAGE}], - }, - { - code: '(class Foo extends Error {})', - errors: [{message: INVALID_OWN_NAME_MESSAGE}], - }, - { - code: 'class Foo extends SomethingEndingWithError {}', - errors: [{message: INVALID_OWN_NAME_MESSAGE}], - }, - { - code: '(class Foo extends SomethingEndingWithError {})', - errors: [{message: INVALID_OWN_NAME_MESSAGE}], - }, - { - code: '(class extends Error {})', - errors: [{message: MISSING_OWN_NAME_MESSAGE}], - }, - { - code: 'class SomethingEndingWithError extends C {}', - errors: [{message: INVALID_SUPERCLASS_MESSAGE}], - }, - { - code: '(class SomethingEndingWithError extends C {})', - errors: [{message: INVALID_SUPERCLASS_MESSAGE}], - }, - { - code: 'function SomethingEndingWithError() {}', - errors: [{message: INVALID_FUNCTION_NAME_MESSAGE}], - }, - { - code: '(function SomethingEndingWithError() {})', - errors: [{message: INVALID_FUNCTION_NAME_MESSAGE}], - }, - - // The following cases are intentionally disallowed because the member - // expression `SomeLibrary.FooError` doesn't imply that the superclass is - // actually declared with the name `FooError`. - { - code: 'class SomethingEndingWithError extends SomeLibrary.FooError {}', - errors: [{message: INVALID_SUPERCLASS_MESSAGE}], - }, - { - code: '(class SomethingEndingWithError extends SomeLibrary.FooError {})', - errors: [{message: INVALID_SUPERCLASS_MESSAGE}], - }, - ], -}); diff --git a/packages/eslint-plugin-react-native-community/error-subclass-name.js b/packages/eslint-plugin-react-native-community/error-subclass-name.js deleted file mode 100644 index 3791436ba813b1..00000000000000 --- a/packages/eslint-plugin-react-native-community/error-subclass-name.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -'use strict'; - -module.exports = function rule(context) { - function classVisitor(node) { - const {superClass, id} = node; - const nodeIsError = isErrorLikeId(id); - const superIsError = isErrorLikeId(superClass); - if (nodeIsError && !superIsError) { - const idName = getNameFromId(id); - context.report({ - node: superClass || id, - message: `'${idName}' must extend an error class (like 'Error') because its name is in PascalCase and ends with 'Error'.`, - }); - } else if (superIsError && !nodeIsError) { - const idName = getNameFromId(id); - context.report({ - node: id || node, - message: idName - ? `'${idName}' may not be the name of an error class. It should be in PascalCase and end with 'Error'.` - : "An error class should have a PascalCase name ending with 'Error'.", - }); - } - } - - function functionVisitor(node) { - const {id} = node; - const nodeIsError = isErrorLikeId(id); - if (nodeIsError) { - const idName = getNameFromId(id); - context.report({ - node: id, - message: `'${idName}' is a reserved name. PascalCase names ending with 'Error' are reserved for error classes and may not be used for regular functions. Either rename this function or convert it to a class that extends 'Error'.`, - }); - } - } - - return { - ClassDeclaration: classVisitor, - ClassExpression: classVisitor, - FunctionExpression: functionVisitor, - FunctionDeclaration: functionVisitor, - }; -}; - -// Checks whether `node` is an identifier (or similar name node) with a -// PascalCase name ending with 'Error'. -function isErrorLikeId(node) { - return ( - node && node.type === 'Identifier' && /^([A-Z].*)?Error$/.test(node.name) - ); -} - -// If `node` is an identifier (or similar name node), returns its name as a -// string. Otherwise returns null. -function getNameFromId(node) { - return node ? node.name : null; -} diff --git a/packages/eslint-plugin-react-native-community/index.js b/packages/eslint-plugin-react-native-community/index.js index b2649b98bbf729..5c3756184557fd 100644 --- a/packages/eslint-plugin-react-native-community/index.js +++ b/packages/eslint-plugin-react-native-community/index.js @@ -8,6 +8,5 @@ */ exports.rules = { - 'error-subclass-name': require('./error-subclass-name'), 'platform-colors': require('./platform-colors'), }; diff --git a/tools/eslint/rules/__tests__/require-extends-error-test.js b/tools/eslint/rules/__tests__/require-extends-error-test.js new file mode 100644 index 00000000000000..198c8603c8eb30 --- /dev/null +++ b/tools/eslint/rules/__tests__/require-extends-error-test.js @@ -0,0 +1,151 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const rule = require('../require-extends-error.js'); +const {RuleTester} = require('eslint'); + +const ruleTester = new RuleTester({ + parser: require.resolve('hermes-eslint'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, +}); + +ruleTester.run('functions', rule, { + valid: [ + { + code: `(function () {});`, + }, + { + code: `function xError() {}`, + }, + ], + invalid: [ + { + code: 'function XError() {}', + errors: [{messageId: 'errorFunction', data: {name: 'XError'}}], + }, + { + code: '(function XError() {});', + errors: [{messageId: 'errorFunction', data: {name: 'XError'}}], + }, + ], +}); + +ruleTester.run('classes', rule, { + valid: [ + { + code: `(class {});`, + }, + { + code: `(class extends Y {});`, + }, + { + code: `class X extends Y {}`, + }, + { + code: `(class X extends Y {});`, + }, + { + code: `class XError extends Error {}`, + }, + { + code: `(class XError extends Error {});`, + }, + { + code: `class XError extends YError {}`, + }, + { + code: `(class XError extends YError {});`, + }, + { + code: `class XError extends Y.Error {}`, + }, + { + code: `(class XError extends Y.Error {});`, + }, + { + code: `class XError extends Y.Z.Error {}`, + }, + { + code: `(class XError extends Y.Z.Error {});`, + }, + ], + invalid: [ + { + code: `class XError {}`, + errors: [{messageId: 'errorClass', data: {name: 'XError'}}], + }, + { + code: `(class XError {});`, + errors: [{messageId: 'errorClass', data: {name: 'XError'}}], + }, + { + code: `class XError extends Y {}`, + errors: [{messageId: 'errorClass', data: {name: 'XError'}}], + }, + { + code: `(class XError extends Y {});`, + errors: [{messageId: 'errorClass', data: {name: 'XError'}}], + }, + { + code: `class XError extends Y.Z {}`, + errors: [{messageId: 'errorClass', data: {name: 'XError'}}], + }, + { + code: `(class XError extends Y.Z {});`, + errors: [{messageId: 'errorClass', data: {name: 'XError'}}], + }, + ], +}); + +ruleTester.run('superclasses', rule, { + valid: [], + invalid: [ + { + code: `(class extends Error {});`, + errors: [{messageId: 'errorSuperClassMissingName'}], + }, + { + code: `class X extends Error {}`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + { + code: `(class X extends Error {});`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + { + code: `class X extends YError {}`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + { + code: `(class X extends YError {});`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + { + code: `class X extends Y.Error {}`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + { + code: `(class X extends Y.Error {});`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + { + code: `class X extends Y.Z.Error {}`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + { + code: `(class X extends Y.Z.Error {});`, + errors: [{messageId: 'errorSuperClass', data: {name: 'X'}}], + }, + ], +}); diff --git a/tools/eslint/rules/require-extends-error.js b/tools/eslint/rules/require-extends-error.js new file mode 100644 index 00000000000000..10acee541e9ac3 --- /dev/null +++ b/tools/eslint/rules/require-extends-error.js @@ -0,0 +1,109 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +module.exports = { + meta: { + type: 'problem', + docs: { + description: "Require error classes to extend 'Error'", + recommended: true, + }, + messages: { + errorClass: + "'{{name}}' must extend an error class (like 'Error') because its name is in PascalCase and ends with 'Error'.", + errorSuperClass: + "'{{name}}' may not be the name of an error class. It should be in PascalCase and end with 'Error'.", + errorSuperClassMissingName: + "An error class should have a PascalCase name ending with 'Error'.", + errorFunction: + "'{{name}}' is a reserved name. PascalCase names ending with 'Error' are reserved for error classes and may not be used for regular functions. Either rename this function or convert it to a class that extends 'Error'.", + }, + schema: [], + }, + + create(context) { + return { + ClassDeclaration(node) { + processClass(node); + }, + ClassExpression(node) { + processClass(node); + }, + FunctionExpression(node) { + processFunction(node); + }, + FunctionDeclaration(node) { + processFunction(node); + }, + }; + + function processClass(node) { + const {id, superClass} = node; + + // First, handle all cases in which superclass is not error-like. + if (superClass == null || !isSuperClassErrorLike(superClass)) { + if (id == null || !isIdentifierErrorLike(id)) { + return; + } + context.report({ + node: id, + messageId: 'errorClass', + data: {name: id.name}, + }); + return; + } + + // Then, handle all cases in which superclass is error-like. + if (id == null) { + context.report({ + node, + messageId: 'errorSuperClassMissingName', + }); + } else if (!isIdentifierErrorLike(id)) { + context.report({ + node: id, + messageId: 'errorSuperClass', + data: {name: id.name}, + }); + } + } + + function processFunction(node) { + const {id} = node; + if (id == null || !isIdentifierErrorLike(id)) { + return; + } + context.report({ + node: id, + messageId: 'errorFunction', + data: {name: id.name}, + }); + } + }, +}; + +function isSuperClassErrorLike(node) { + if (node.type === 'Identifier') { + return isNameErrorLike(node.name); + } + if (node.type === 'MemberExpression') { + return isSuperClassErrorLike(node.property); + } + return false; +} + +function isIdentifierErrorLike(node) { + return node.type === 'Identifier' && isNameErrorLike(node.name); +} + +function isNameErrorLike(name) { + return typeof name === 'string' && /^([A-Z].*)?Error$/.test(name); +}