Skip to content

Commit

Permalink
chore: Enforce the use of ReanimatedError instead of Error (#6454)
Browse files Browse the repository at this point in the history
## Summary

This PR adds eslint rule that will enforce the use of the new
`ReanimatedError` in favor of `Error` in the library source files.

## Example image

![Screenshot 2024-08-28 at 17 25
11](https://github.com/user-attachments/assets/c11043ba-cfb4-43da-93d5-ea79611d1421)

## Test plan

Just try to use `throw new Error` inside the
`packages/react-native-reanimated` instead of `throw new
ReanimatedError` and see that eslint complains about incorrect usage.
  • Loading branch information
MatiPl01 authored Aug 30, 2024
1 parent b7f53e6 commit 4f5b96a
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-disable */
// @ts-nocheck
// TODO: FIX THESE
// eslint-disable-next-line import/no-unresolved
import { RuleTester } from '@typescript-eslint/rule-tester';
import { rules } from '../src';

// For reasons unknown the following line causes Jest to hang indefinitely
// hence we disable this test suite until it's resolved
// const ruleTester = new RuleTester({
// parserOptions: {
// ecmaFeatures: {
// jsx: true,
// },
// sourceType: 'module',
// },
// parser: '@typescript-eslint/parser',
// });

// const testCases = {
// withReanimatedError: {
// /** Correct code correctly classified as satisfying the rule */
// trueNegative: [
// `const err = new ReanimatedError('Something went wrong');`,
// `function createError() { return new ReanimatedError('Error message'); }`,
// `throw new ReanimatedError('Custom error');`,
// ],
// /** Incorrect code correctly classified as not satisfying the rule */
// truePositive: [
// {
// code: `const err = new Error('Something went wrong');`,
// errors: [{ messageId: 'useReanimatedError' }],
// output: `const err = new ReanimatedError('Something went wrong');`,
// },
// {
// code: `function createError() { return new Error('Error message'); }`,
// errors: [{ messageId: 'useReanimatedError' }],
// output: `function createError() { return new ReanimatedError('Error message'); }`,
// },
// {
// code: `throw new Error('Custom error');`,
// errors: [{ messageId: 'useReanimatedError' }],
// output: `throw new ReanimatedError('Custom error');`,
// },
// ],
// /** Incorrect code incorrectly classified as satisfying the rule */
// falseNegative: [],
// /** Incorrect code incorrectly classified as not satisfying the rule */
// falsePositive: [],
// },
// };

// const { withReanimatedError } = testCases;

// const ruleName = 'use-reanimated-error';
// ruleTester.run(`Test rule ${ruleName}`, rules[ruleName], {
// valid: [...withReanimatedError.trueNegative],
// invalid: [...withReanimatedError.truePositive],
// });
47 changes: 47 additions & 0 deletions packages/eslint-plugin-reanimated/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,51 @@ var require_noAnimatedStyleToNonAnimatedComponent = __commonJS({
},
});

// public/useReanimatedError.js
var require_useReanimatedError = __commonJS({
'public/useReanimatedError.js'(exports2) {
'use strict';
Object.defineProperty(exports2, '__esModule', { value: true });
var utils_1 = require('@typescript-eslint/utils');
var rule = {
create: function (context) {
return {
NewExpression(node) {
if (
node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
node.callee.name === 'Error'
) {
context.report({
node,
messageId: 'useReanimatedError',
fix: function (fixer) {
return fixer.replaceText(node.callee, 'ReanimatedError');
},
});
}
},
};
},
meta: {
docs: {
recommended: 'recommended',
description:
'Warns when `new Error` is used instead of `new ReanimatedError`.',
},
messages: {
useReanimatedError:
'Use `new ReanimatedError` instead of `new Error`.',
},
type: 'suggestion',
schema: [],
fixable: 'code',
},
defaultOptions: [],
};
exports2.default = rule;
},
});

// public/index.js
var __importDefault =
(exports && exports.__importDefault) ||
Expand All @@ -181,7 +226,9 @@ exports.rules = void 0;
var noAnimatedStyleToNonAnimatedComponent_1 = __importDefault(
require_noAnimatedStyleToNonAnimatedComponent()
);
var useReanimatedError_1 = __importDefault(require_useReanimatedError());
exports.rules = {
'animated-style-non-animated-component':
noAnimatedStyleToNonAnimatedComponent_1.default,
'use-reanimated-error': useReanimatedError_1.default,
};
2 changes: 2 additions & 0 deletions packages/eslint-plugin-reanimated/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { TSESLint } from '@typescript-eslint/utils';
import noAnimatedStyleToNonAnimatedComponent from './noAnimatedStyleToNonAnimatedComponent';
import useReanimatedError from './useReanimatedError';

export const rules = {
'animated-style-non-animated-component':
noAnimatedStyleToNonAnimatedComponent,
'use-reanimated-error': useReanimatedError,
} satisfies Record<string, TSESLint.RuleModule<string, Array<unknown>>>;
42 changes: 42 additions & 0 deletions packages/eslint-plugin-reanimated/src/useReanimatedError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
// eslint-disable-next-line import/no-unresolved
import { AST_NODE_TYPES } from '@typescript-eslint/utils';

const rule: TSESLint.RuleModule<'useReanimatedError', []> = {
create: function (context) {
return {
NewExpression(node: TSESTree.NewExpression) {
// Check if the expression is `new Error`
if (
node.callee.type === AST_NODE_TYPES.Identifier &&
node.callee.name === 'Error'
) {
context.report({
node,
messageId: 'useReanimatedError',
fix: function (fixer) {
// Replace `Error` with `ReanimatedError`
return fixer.replaceText(node.callee, 'ReanimatedError');
},
});
}
},
};
},
meta: {
docs: {
recommended: 'recommended',
description:
'Warns when `new Error` is used instead of `new ReanimatedError`.',
},
messages: {
useReanimatedError: 'Use `new ReanimatedError` instead of `new Error`.',
},
type: 'suggestion',
schema: [],
fixable: 'code',
},
defaultOptions: [],
};

export default rule;
5 changes: 5 additions & 0 deletions packages/eslint-plugin-reanimated/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ export declare const rules: {
[],
TSESLint.RuleListener
>;
'use-reanimated-error': TSESLint.RuleModule<
'useReanimatedError',
[],
TSESLint.RuleListener
>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { TSESLint } from '@typescript-eslint/utils';
declare const rule: TSESLint.RuleModule<'useReanimatedError', []>;
export default rule;
4 changes: 4 additions & 0 deletions packages/react-native-reanimated/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module.exports = {
extends: ['../../.eslintrc.js'],
plugins: ['eslint-plugin-reanimated'],
ignorePatterns: ['lib'],
rules: {
'reanimated/use-reanimated-error': 'error',
},
};
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable reanimated/use-reanimated-error */
'use strict';
import type { WorkletStackDetails } from './commonTypes';

Expand Down
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/logger/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function createLog(level: LogBoxLogLevel, message: string): LogData {
category: formattedMessage,
componentStack: [],
componentStackType: null,
// eslint-disable-next-line reanimated/use-reanimated-error
stack: new Error().stack,
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/shareables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ Offending code was: \`${getWorkletCode(value)}\``);
const handle = makeShareableCloneRecursive({
__init: () => {
'worklet';
// eslint-disable-next-line reanimated/use-reanimated-error
const error = new Error();
error.name = name;
error.message = message;
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-reanimated/src/valueUnpacker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable reanimated/use-reanimated-error */
'use strict';
import { shouldBeUseWeb } from './PlatformChecker';
import { isWorkletFunction } from './commonTypes';
Expand Down

0 comments on commit 4f5b96a

Please sign in to comment.