diff --git a/src/configs/recommended.json b/src/configs/recommended.json index af5ed45c..a6be8c37 100755 --- a/src/configs/recommended.json +++ b/src/configs/recommended.json @@ -9,6 +9,7 @@ "ngrx/no-multiple-stores": "error", "ngrx/no-dispatch-in-effects": "error", "ngrx/no-effect-decorator": "error", - "ngrx/no-effects-in-providers": "error" + "ngrx/no-effects-in-providers": "error", + "ngrx/use-selector-in-select": "error" } } diff --git a/src/rules/index.ts b/src/rules/index.ts index 888b1245..43e8644d 100755 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -25,6 +25,9 @@ import noEffectDecorator, { import noEffectsInProviders, { ruleName as noEffectsInProvidersRuleName, } from './no-effects-in-providers' +import useSelectorInSelect, { + ruleName as useSelectorInSelectRuleName, +} from './use-selector-in-select' const ruleNames = { actionHygieneRuleName, @@ -36,6 +39,7 @@ const ruleNames = { noDispatchInEffectsRuleName, noEffectDecoratorRuleName, noEffectsInProvidersRuleName, + useSelectorInSelectRuleName, } export const rules = { @@ -48,4 +52,5 @@ export const rules = { [ruleNames.noDispatchInEffectsRuleName]: noDispatchInEffects, [ruleNames.noEffectDecoratorRuleName]: noEffectDecorator, [ruleNames.noEffectsInProvidersRuleName]: noEffectsInProviders, + [ruleNames.useSelectorInSelectRuleName]: useSelectorInSelect, } diff --git a/src/rules/use-selector-in-select.ts b/src/rules/use-selector-in-select.ts new file mode 100644 index 00000000..129332db --- /dev/null +++ b/src/rules/use-selector-in-select.ts @@ -0,0 +1,42 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils' + +import { select } from './utils' + +export const ruleName = 'use-selector-in-select' + +export const messageId = 'useSelectorInSelect' +export type MessageIds = typeof messageId + +type Options = [] + +export default ESLintUtils.RuleCreator(name => name)({ + name: ruleName, + meta: { + type: 'problem', + docs: { + category: 'Possible Errors', + description: + 'Using a selector in a select function is preferred in favor of strings/props drilling', + extraDescription: [ + 'A selector is more performant, shareable and maintainable', + ], + recommended: 'error', + }, + schema: [], + messages: { + [messageId]: + 'Using string or props drilling is not preferred, use a selector instead', + }, + }, + defaultOptions: [], + create: context => { + return { + [select](node: TSESTree.Literal | TSESTree.ArrowFunctionExpression) { + context.report({ + node, + messageId, + }) + }, + } + }, +}) diff --git a/src/rules/utils/selectors/index.ts b/src/rules/utils/selectors/index.ts index 0269d54e..da4d77a4 100644 --- a/src/rules/utils/selectors/index.ts +++ b/src/rules/utils/selectors/index.ts @@ -23,3 +23,8 @@ export const ngModuleDecorator = `ClassDeclaration > Decorator > CallExpression[ export const ngModuleProviders = `${ngModuleDecorator} ObjectExpression Property[key.name='providers'] > ArrayExpression Identifier` export const ngModuleImports = `${ngModuleDecorator} ObjectExpression Property[key.name='imports'] > ArrayExpression CallExpression[callee.object.name='EffectsModule'][callee.property.name=/forRoot|forFeature/] ArrayExpression > Identifier` + +const pipeableSelect = `CallExpression[callee.property.name="pipe"] CallExpression[callee.name="select"]` +const storeSelect = `CallExpression[callee.object.name='store'][callee.property.name='select']` + +export const select = `${pipeableSelect} Literal, ${storeSelect} Literal, ${pipeableSelect} ArrowFunctionExpression, ${storeSelect} ArrowFunctionExpression` diff --git a/tests/rules/use-selector-in-select.test.ts b/tests/rules/use-selector-in-select.test.ts new file mode 100644 index 00000000..ff7246e4 --- /dev/null +++ b/tests/rules/use-selector-in-select.test.ts @@ -0,0 +1,135 @@ +import { stripIndent } from 'common-tags' +import rule, { + ruleName, + messageId, +} from '../../src/rules/use-selector-in-select' +import { ruleTester } from '../utils' + +ruleTester().run(ruleName, rule, { + valid: [ + `store.pipe(select(selectCustomers))`, + `store.pipe(select(selectorsObj.selectCustomers))`, + `store.select(selectCustomers)`, + `store.select(selectorsObj.selectCustomers)`, + ], + invalid: [ + { + code: stripIndent` + store.pipe(select('customers'))`, + errors: [ + { + messageId, + line: 1, + column: 19, + endLine: 1, + endColumn: 30, + }, + ], + }, + { + code: stripIndent` + store.select('customers')`, + errors: [ + { + messageId, + line: 1, + column: 14, + endLine: 1, + endColumn: 25, + }, + ], + }, + { + code: stripIndent` + store.pipe(select('customers', 'orders'))`, + errors: [ + { + messageId, + line: 1, + column: 19, + endLine: 1, + endColumn: 30, + }, + { + messageId, + line: 1, + column: 32, + endLine: 1, + endColumn: 40, + }, + ], + }, + { + code: stripIndent` + store.select('customers', 'orders')`, + errors: [ + { + messageId, + line: 1, + column: 14, + endLine: 1, + endColumn: 25, + }, + { + messageId, + line: 1, + column: 27, + endLine: 1, + endColumn: 35, + }, + ], + }, + { + code: stripIndent` + store.pipe(select(state => state.customers))`, + errors: [ + { + messageId, + line: 1, + column: 19, + endLine: 1, + endColumn: 43, + }, + ], + }, + { + code: stripIndent` + store.select(state => state.customers)`, + errors: [ + { + messageId, + line: 1, + column: 14, + endLine: 1, + endColumn: 38, + }, + ], + }, + { + code: stripIndent` + store.pipe(select(state => state.customers.orders))`, + errors: [ + { + messageId, + line: 1, + column: 19, + endLine: 1, + endColumn: 50, + }, + ], + }, + { + code: stripIndent` + store.select(state => state.customers.orders)`, + errors: [ + { + messageId, + line: 1, + column: 14, + endLine: 1, + endColumn: 45, + }, + ], + }, + ], +})