Skip to content

Commit 6611c4b

Browse files
motiz88facebook-github-bot
authored andcommitted
Move error-subclass-name lint rule to GitHub
Summary: Ports an internal ESLint rule used at Facebook, `error-subclass-name`, to cover the React Native codebase. This rule enforces that error classes ( = those 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. Reviewed By: rubennorte Differential Revision: D17829298 fbshipit-source-id: 834e457343034a0897ab394b6a2d941789953d2e
1 parent 1dc03f4 commit 6611c4b

File tree

9 files changed

+228
-5
lines changed

9 files changed

+228
-5
lines changed

.eslintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"Libraries/**/*.js",
1212
],
1313
rules: {
14-
'@react-native-community/no-haste-imports': 2
14+
'@react-native-community/no-haste-imports': 2,
15+
'@react-native-community/error-subclass-name': 2,
1516
}
1617
},
1718
{

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
"devDependencies": {
120120
"@babel/core": "^7.0.0",
121121
"@babel/generator": "^7.0.0",
122-
"@react-native-community/eslint-plugin": "1.0.0",
122+
"@react-native-community/eslint-plugin": "file:packages/eslint-plugin-react-native-community",
123123
"@reactions/component": "^2.0.2",
124124
"async": "^2.4.0",
125125
"babel-eslint": "10.0.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
load("@fbsource//tools/build_defs/third_party:yarn_defs.bzl", "yarn_workspace")
2+
3+
yarn_workspace(
4+
name = "yarn-workspace",
5+
srcs = glob(
6+
["**/*.js"],
7+
exclude = [
8+
"**/__fixtures__/**",
9+
"**/__flowtests__/**",
10+
"**/__mocks__/**",
11+
"**/__server_snapshot_tests__/**",
12+
"**/__tests__/**",
13+
"**/node_modules/**",
14+
"**/node_modules/.bin/**",
15+
"**/.*",
16+
"**/.*/**",
17+
"**/.*/.*",
18+
"**/*.xcodeproj/**",
19+
"**/*.xcworkspace/**",
20+
],
21+
),
22+
visibility = ["PUBLIC"],
23+
)

packages/eslint-plugin-react-native-community/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,15 @@ Add to your eslint config (`.eslintrc`, or `eslintConfig` field in `package.json
1919
"plugins": ["@react-native-community"]
2020
}
2121
```
22+
23+
## Rules
24+
25+
### `error-subclass-name`
26+
27+
**NOTE:** This rule is primarily used for developing React Native itself and is not generally applicable to other projects.
28+
29+
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.
30+
31+
### `no-haste-imports`
32+
33+
Disallows Haste module names in `import` statements and `require()` calls.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails oncall+react_native
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
const ESLintTester = require('./eslint-tester.js');
14+
15+
const rule = require('../error-subclass-name.js');
16+
17+
const eslintTester = new ESLintTester();
18+
19+
const INVALID_SUPERCLASS_MESSAGE =
20+
"'SomethingEndingWithError' must extend an error class (like 'Error') because its name is in PascalCase and ends with 'Error'.";
21+
const INVALID_OWN_NAME_MESSAGE =
22+
"'Foo' may not be the name of an error class. It should be in PascalCase and end with 'Error'.";
23+
const MISSING_OWN_NAME_MESSAGE =
24+
"An error class should have a PascalCase name ending with 'Error'.";
25+
const INVALID_FUNCTION_NAME_MESSAGE =
26+
"'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'.";
27+
28+
eslintTester.run('../error-subclass-name', rule, {
29+
valid: [
30+
'class FooError extends Error {}',
31+
'(class FooError extends Error {})',
32+
'class FooError extends SomethingEndingWithError {}',
33+
'(class FooError extends SomethingEndingWithError {})',
34+
'function makeError() {}',
35+
'(function () {})',
36+
37+
// The following cases are currently allowed but could be disallowed in the
38+
// future. This is technically an escape hatch.
39+
'class Foo extends SomeLibrary.FooError {}',
40+
'(class extends SomeLibrary.FooError {})',
41+
],
42+
invalid: [
43+
{
44+
code: 'class SomethingEndingWithError {}',
45+
errors: [{message: INVALID_SUPERCLASS_MESSAGE}],
46+
},
47+
{
48+
code: '(class SomethingEndingWithError {})',
49+
errors: [{message: INVALID_SUPERCLASS_MESSAGE}],
50+
},
51+
{
52+
code: 'class Foo extends Error {}',
53+
errors: [{message: INVALID_OWN_NAME_MESSAGE}],
54+
},
55+
{
56+
code: '(class Foo extends Error {})',
57+
errors: [{message: INVALID_OWN_NAME_MESSAGE}],
58+
},
59+
{
60+
code: 'class Foo extends SomethingEndingWithError {}',
61+
errors: [{message: INVALID_OWN_NAME_MESSAGE}],
62+
},
63+
{
64+
code: '(class Foo extends SomethingEndingWithError {})',
65+
errors: [{message: INVALID_OWN_NAME_MESSAGE}],
66+
},
67+
{
68+
code: '(class extends Error {})',
69+
errors: [{message: MISSING_OWN_NAME_MESSAGE}],
70+
},
71+
{
72+
code: 'class SomethingEndingWithError extends C {}',
73+
errors: [{message: INVALID_SUPERCLASS_MESSAGE}],
74+
},
75+
{
76+
code: '(class SomethingEndingWithError extends C {})',
77+
errors: [{message: INVALID_SUPERCLASS_MESSAGE}],
78+
},
79+
{
80+
code: 'function SomethingEndingWithError() {}',
81+
errors: [{message: INVALID_FUNCTION_NAME_MESSAGE}],
82+
},
83+
{
84+
code: '(function SomethingEndingWithError() {})',
85+
errors: [{message: INVALID_FUNCTION_NAME_MESSAGE}],
86+
},
87+
88+
// The following cases are intentionally disallowed because the member
89+
// expression `SomeLibrary.FooError` doesn't imply that the superclass is
90+
// actually declared with the name `FooError`.
91+
{
92+
code: 'class SomethingEndingWithError extends SomeLibrary.FooError {}',
93+
errors: [{message: INVALID_SUPERCLASS_MESSAGE}],
94+
},
95+
{
96+
code: '(class SomethingEndingWithError extends SomeLibrary.FooError {})',
97+
errors: [{message: INVALID_SUPERCLASS_MESSAGE}],
98+
},
99+
],
100+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
const ESLintTester = require('eslint').RuleTester;
13+
14+
ESLintTester.setDefaultConfig({
15+
parser: 'babel-eslint',
16+
parserOptions: {
17+
ecmaVersion: 6,
18+
sourceType: 'module',
19+
},
20+
});
21+
22+
module.exports = ESLintTester;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
module.exports = function rule(context) {
13+
function classVisitor(node) {
14+
const {superClass, id} = node;
15+
const nodeIsError = isErrorLikeId(id);
16+
const superIsError = isErrorLikeId(superClass);
17+
if (nodeIsError && !superIsError) {
18+
const idName = getNameFromId(id);
19+
context.report({
20+
node: superClass || id,
21+
message: `'${idName}' must extend an error class (like 'Error') because its name is in PascalCase and ends with 'Error'.`,
22+
});
23+
} else if (superIsError && !nodeIsError) {
24+
const idName = getNameFromId(id);
25+
context.report({
26+
node: id || node,
27+
message: idName
28+
? `'${idName}' may not be the name of an error class. It should be in PascalCase and end with 'Error'.`
29+
: "An error class should have a PascalCase name ending with 'Error'.",
30+
});
31+
}
32+
}
33+
34+
function functionVisitor(node) {
35+
const {id} = node;
36+
const nodeIsError = isErrorLikeId(id);
37+
if (nodeIsError) {
38+
const idName = getNameFromId(id);
39+
context.report({
40+
node: id,
41+
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'.`,
42+
});
43+
}
44+
}
45+
46+
return {
47+
ClassDeclaration: classVisitor,
48+
ClassExpression: classVisitor,
49+
FunctionExpression: functionVisitor,
50+
FunctionDeclaration: functionVisitor,
51+
};
52+
};
53+
54+
// Checks whether `node` is an identifier (or similar name node) with a
55+
// PascalCase name ending with 'Error'.
56+
function isErrorLikeId(node) {
57+
return (
58+
node && node.type === 'Identifier' && /^([A-Z].*)?Error$/.test(node.name)
59+
);
60+
}
61+
62+
// If `node` is an identifier (or similar name node), returns its name as a
63+
// string. Otherwise returns null.
64+
function getNameFromId(node) {
65+
return node ? node.name : null;
66+
}

packages/eslint-plugin-react-native-community/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
*/
99

1010
exports.rules = {
11+
'error-subclass-name': require('./error-subclass-name'),
1112
'no-haste-imports': require('./no-haste-imports'),
1213
};

yarn.lock

+1-3
Original file line numberDiff line numberDiff line change
@@ -1145,10 +1145,8 @@
11451145
shell-quote "1.6.1"
11461146
ws "^1.1.0"
11471147

1148-
"@react-native-community/eslint-plugin@1.0.0":
1148+
"@react-native-community/eslint-plugin@file:packages/eslint-plugin-react-native-community":
11491149
version "1.0.0"
1150-
resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.0.0.tgz#ae9a430f2c5795debca491f15a989fce86ea75a0"
1151-
integrity sha512-GLhSN8dRt4lpixPQh+8prSCy6PYk/MT/mvji/ojAd5yshowDo6HFsimCSTD/uWAdjpUq91XK9tVdTNWfGRlKQA==
11521150

11531151
"@reactions/component@^2.0.2":
11541152
version "2.0.2"

0 commit comments

Comments
 (0)