-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
integrate no-use-state-initializer-functions
- Loading branch information
Showing
3 changed files
with
155 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,62 @@ | ||
/* eslint-disable max-len */ | ||
module.exports = { | ||
MESSAGE: { | ||
DISPLAY_NAME_PROPERTY_CLASS: 'Class components must not set the displayName property.', | ||
DISPLAY_NAME_PROPERTY_FUNCTION: 'Function components must set the displayName property.', | ||
NO_API_IN_VIEWS: 'Do not call API directly outside of actions methods. Only actions should make API requests.', | ||
DISPLAY_NAME_PROPERTY_CLASS: | ||
'Class components must not set the displayName property.', | ||
DISPLAY_NAME_PROPERTY_FUNCTION: | ||
'Function components must set the displayName property.', | ||
NO_API_IN_VIEWS: | ||
'Do not call API directly outside of actions methods. Only actions should make API requests.', | ||
NO_INLINE_NAMED_EXPORT: 'Do not inline named exports.', | ||
NO_NEGATED_VARIABLES: 'Do not use negated variable names.', | ||
NO_THENABLE_ACTIONS_IN_VIEWS: 'Calling .then() on action method {{method}} is forbidden in React views. Relocate this logic into the actions file and pass values via Onyx.', | ||
NO_USELESS_COMPOSE: 'compose() is not necessary when passed a single argument', | ||
NO_MULTIPLE_ONYX_IN_FILE: 'Only use withOnyx() once. Read here about how to properly work with dependent Onyx keys: https://github.com/Expensify/react-native-onyx#dependent-onyx-keys-and-withonyx', | ||
PREFER_ACTIONS_SET_DATA: 'Only actions should directly set or modify Onyx data. Please move this logic into a suitable action.', | ||
PREFER_EARLY_RETURN: 'Prefer an early return to a conditionally-wrapped function body', | ||
PREFER_IMPORT_MODULE_CONTENTS: 'Do not import individual exports from local modules. Prefer \'import * as\' syntax.', | ||
PREFER_ONYX_CONNECT_IN_LIBS: 'Only call Onyx.connect() from inside a /src/libs/** file. React components and non-library code should not use Onyx.connect()', | ||
PREFER_UNDERSCORE_METHOD: 'Prefer \'_.{{method}}\' over the native function.', | ||
PREFER_STR_METHOD: 'Prefer \'Str.{{method}}\' over the native function.', | ||
PREFER_LOCALIZATION: 'Use Localize.translateLocal(\'i18n.key\') over hard coded messages.', | ||
NO_MULTIPLE_API_CALLS: 'Do not call API multiple times in the same method. The API response should return all the necessary data in a single request, and API calls should not be chained together.', | ||
NO_CALL_ACTIONS_FROM_ACTIONS: 'Calling actions from inside other actions is forbidden. If an action needs to call another action combine the two actions into a singular API call instead.', | ||
NO_THENABLE_ACTIONS_IN_VIEWS: | ||
'Calling .then() on action method {{method}} is forbidden in React views. Relocate this logic into the actions file and pass values via Onyx.', | ||
NO_USELESS_COMPOSE: | ||
'compose() is not necessary when passed a single argument', | ||
NO_MULTIPLE_ONYX_IN_FILE: | ||
'Only use withOnyx() once. Read here about how to properly work with dependent Onyx keys: https://github.com/Expensify/react-native-onyx#dependent-onyx-keys-and-withonyx', | ||
PREFER_ACTIONS_SET_DATA: | ||
'Only actions should directly set or modify Onyx data. Please move this logic into a suitable action.', | ||
PREFER_EARLY_RETURN: | ||
'Prefer an early return to a conditionally-wrapped function body', | ||
PREFER_IMPORT_MODULE_CONTENTS: | ||
"Do not import individual exports from local modules. Prefer 'import * as' syntax.", | ||
PREFER_ONYX_CONNECT_IN_LIBS: | ||
'Only call Onyx.connect() from inside a /src/libs/** file. React components and non-library code should not use Onyx.connect()', | ||
PREFER_UNDERSCORE_METHOD: | ||
"Prefer '_.{{method}}' over the native function.", | ||
PREFER_STR_METHOD: "Prefer 'Str.{{method}}' over the native function.", | ||
PREFER_LOCALIZATION: | ||
"Use Localize.translateLocal('i18n.key') over hard coded messages.", | ||
NO_MULTIPLE_API_CALLS: | ||
'Do not call API multiple times in the same method. The API response should return all the necessary data in a single request, and API calls should not be chained together.', | ||
NO_CALL_ACTIONS_FROM_ACTIONS: | ||
'Calling actions from inside other actions is forbidden. If an action needs to call another action combine the two actions into a singular API call instead.', | ||
NO_API_SIDE_EFFECTS_METHOD: 'Do not use makeRequestWithSideEffects.', | ||
PROP_TYPE_REQUIRED_FALSE: 'Prop {{key}} should not be explicitly required.', | ||
PROP_TYPE_REQUIRED_FALSE: | ||
'Prop {{key}} should not be explicitly required.', | ||
PROP_TYPE_NOT_DECLARED: 'Prop {{key}} must be declared in propTypes.', | ||
PROP_DEEFAULT_NOT_DECLARED: 'Prop {{key}} must have a default value.', | ||
HAVE_PROP_TYPES: 'Component must have prop types declared.', | ||
HAVE_DEFAULT_PROPS: 'Component must have default prop values.', | ||
ONYX_ONE_PARAM: 'The withOnyx HOC must be passed at least one argument.', | ||
MUST_USE_VARIABLE_FOR_ASSIGNMENT: '{{key}} must be assigned as a variable instead of direct assignment.', | ||
NO_DEFAULT_PROPS: 'defaultProps should not be used in function components. Use default Arguments instead.', | ||
ONYX_ONE_PARAM: | ||
'The withOnyx HOC must be passed at least one argument.', | ||
MUST_USE_VARIABLE_FOR_ASSIGNMENT: | ||
'{{key}} must be assigned as a variable instead of direct assignment.', | ||
NO_DEFAULT_PROPS: | ||
'defaultProps should not be used in function components. Use default Arguments instead.', | ||
USE_PERIODS_ERROR_MESSAGES: 'Use periods at the end of error messages.', | ||
USE_DOUBLE_NEGATION_INSTEAD_OF_BOOLEAN: 'Use !! instead of Boolean().', | ||
NO_ACC_SPREAD_IN_REDUCE: 'Avoid a use of spread (`...`) operator on accumulators in reduce callback. Mutate them directly instead.', | ||
PREFER_TYPE_FEST_TUPLE_TO_UNION: 'Prefer using `TupleToUnion` from `type-fest` for converting tuple types to union types.', | ||
PREFER_TYPE_FEST_VALUE_OF: 'Prefer using `ValueOf` from `type-fest` to extract the type of the properties of an object.', | ||
NO_ACC_SPREAD_IN_REDUCE: | ||
'Avoid a use of spread (`...`) operator on accumulators in reduce callback. Mutate them directly instead.', | ||
PREFER_TYPE_FEST_TUPLE_TO_UNION: | ||
'Prefer using `TupleToUnion` from `type-fest` for converting tuple types to union types.', | ||
PREFER_TYPE_FEST_VALUE_OF: | ||
'Prefer using `ValueOf` from `type-fest` to extract the type of the properties of an object.', | ||
PREFER_AT: 'Prefer using the `.at()` method for array element access.', | ||
PREFER_SHOULD_USE_NARROW_LAYOUT_INSTEAD_OF_IS_SMALL_SCREEN_WIDTH: 'Prefer using `shouldUseNarrowLayout` instead of `isSmallScreenWidth` from `useResponsiveLayout`.', | ||
PREFER_SHOULD_USE_NARROW_LAYOUT_INSTEAD_OF_IS_SMALL_SCREEN_WIDTH: | ||
'Prefer using `shouldUseNarrowLayout` instead of `isSmallScreenWidth` from `useResponsiveLayout`.', | ||
NO_USE_STATE_INITIALIZER_CALL_FUNCTION: | ||
'Avoid calling a function directly in the useState initializer. Use an initializer function instead (a callback).', | ||
}, | ||
}; |
74 changes: 74 additions & 0 deletions
74
eslint-plugin-expensify/no-use-state-initializer-functions.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
const message = require('./CONST').MESSAGE.NO_USE_STATE_INITIALIZER_CALL_FUNCTION; | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', // This is a potential performance issue | ||
docs: { | ||
description: | ||
'Disallow direct function calls in useState initializer', | ||
category: 'Best Practices', | ||
recommended: false, | ||
}, | ||
schema: [], // No options for this rule | ||
messages: { | ||
noDirectCall: message, | ||
}, | ||
}, | ||
create(context) { | ||
return { | ||
CallExpression(node) { | ||
// Return early if the function being called is not `useState` | ||
if ( | ||
node.callee.type !== 'Identifier' | ||
|| node.callee.name !== 'useState' | ||
|| node.arguments.length === 0 | ||
) { | ||
return; | ||
} | ||
|
||
const firstArg = node.arguments[0]; | ||
|
||
// Return early if the first argument is not a function call | ||
if (firstArg.type !== 'CallExpression') { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node: firstArg, | ||
messageId: 'noDirectCall', | ||
}); | ||
}, | ||
|
||
// Handle cases where the initializer is passed as a function reference | ||
VariableDeclarator(node) { | ||
if ( | ||
!node.init | ||
|| node.init.type !== 'CallExpression' | ||
|| node.init.callee.name !== 'useState' | ||
|| node.init.arguments.length === 0 | ||
) { | ||
return; | ||
} | ||
|
||
const firstArg = node.init.arguments[0]; | ||
|
||
// Return early if the first argument is a function reference (valid case) | ||
if ( | ||
firstArg.type === 'Identifier' // e.g., `getChatTabBrickRoad` | ||
|| (firstArg.type === 'ArrowFunctionExpression' | ||
&& firstArg.body.type !== 'CallExpression') // e.g., `() => getChatTabBrickRoad` | ||
) { | ||
return; // Valid case, do nothing | ||
} | ||
|
||
// If it's a direct function call, report it | ||
if (firstArg.type === 'CallExpression') { | ||
context.report({ | ||
node: firstArg, | ||
messageId: 'noDirectCall', | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
34 changes: 34 additions & 0 deletions
34
eslint-plugin-expensify/tests/no-use-state-initializer-functions.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
const RuleTester = require('eslint').RuleTester; | ||
const rule = require('../no-use-state-initializer-functions'); | ||
const message = require('../CONST').MESSAGE.NO_USE_STATE_INITIALIZER_CALL_FUNCTION; | ||
|
||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
ecmaVersion: 6, | ||
sourceType: 'module', | ||
}, | ||
}); | ||
|
||
ruleTester.run('no-use-state-initializer-functions', rule, { | ||
valid: [ | ||
{ | ||
// Calling a callback should be valid | ||
code: ` | ||
useState(() => testFunc()); | ||
`, | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
// Calling a function should be invalid | ||
code: ` | ||
useState(testFunc()); | ||
`, | ||
errors: [ | ||
{ | ||
message, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |