From c392618d5908afe7ab615f4af03c349602769240 Mon Sep 17 00:00:00 2001 From: Rafael Santana Date: Tue, 12 Oct 2021 14:34:51 -0300 Subject: [PATCH] fix: report rules on multiple instances (#221) --- .eslintrc.js | 1 + scripts/generate-docs.ts | 4 +- .../updater-explicit-return-type.ts | 14 +- src/rules/effects/avoid-cyclic-effects.ts | 13 +- src/rules/effects/no-dispatch-in-effects.ts | 13 +- .../effects/prefer-concat-latest-from.ts | 31 +- src/rules/store/avoid-combining-selectors.ts | 23 +- ...spatching-multiple-actions-sequentially.ts | 21 +- src/rules/store/avoid-mapping-selectors.ts | 23 +- src/rules/store/no-multiple-global-stores.ts | 68 +-- src/rules/store/no-store-subscription.ts | 17 +- src/rules/store/no-typed-global-store.ts | 40 +- .../prefer-action-creator-in-dispatch.ts | 19 +- src/rules/store/select-style.ts | 19 +- .../store/use-consistent-global-store-name.ts | 64 +-- src/rules/store/use-selector-in-select.ts | 25 +- src/utils/helper-functions/guards.ts | 10 +- src/utils/helper-functions/utils.ts | 103 +++-- src/utils/selectors/index.ts | 39 +- tests/rules/avoid-combining-selectors.test.ts | 205 +++++---- ...hing-multiple-actions-sequentially.test.ts | 218 +++++----- tests/rules/avoid-mapping-selectors.test.ts | 273 ++++++------ tests/rules/no-dispatch-in-effects.test.ts | 393 ++++++++++------- tests/rules/no-multiple-global-stores.test.ts | 360 +++++++++------- tests/rules/no-store-subscription.test.ts | 267 ++++++------ tests/rules/no-typed-global-store.test.ts | 225 +++++----- .../prefer-action-creator-in-dispatch.test.ts | 151 ++++--- tests/rules/prefer-concat-latest-from.test.ts | 123 +++--- tests/rules/select-style.test.ts | 407 +++++++++--------- .../updater-explicit-return-type.test.ts | 204 +++++---- .../use-consistent-global-store-name.test.ts | 205 +++++---- tests/rules/use-selector-in-select.test.ts | 233 +++++----- 32 files changed, 2047 insertions(+), 1764 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d945bcf8..5d5bdf7c 100755 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,5 +19,6 @@ module.exports = { '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/prefer-reduce-type-parameter': 'error', + curly: 'error', }, } diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 3ce9f51c..1b892f79 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -9,7 +9,9 @@ const placeholder = '' for (const [ruleName, { meta }] of Object.entries(rules)) { const docPath = path.join('docs', 'rules', `${ruleName}.md`) const doc = readFileSync(docPath, 'utf-8') - if (doc.indexOf(placeholder) === -1) continue + if (doc.indexOf(placeholder) === -1) { + continue + } const docContent = doc.substr(doc.indexOf(placeholder) + placeholder.length) const frontMatter = [ diff --git a/src/rules/component-store/updater-explicit-return-type.ts b/src/rules/component-store/updater-explicit-return-type.ts index 027a5ebc..4c28371c 100644 --- a/src/rules/component-store/updater-explicit-return-type.ts +++ b/src/rules/component-store/updater-explicit-return-type.ts @@ -2,9 +2,10 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, docsUrl, - findNgRxComponentStoreName, - storeExpression, + getNgRxComponentStores, + namedExpression, } from '../../utils' export const messageId = 'updaterExplicitReturnType' @@ -29,13 +30,14 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxComponentStoreName(context) + const { identifiers } = getNgRxComponentStores(context) + const storeNames = identifiers?.length ? asPattern(identifiers) : null const withoutTypeAnnotation = `ArrowFunctionExpression:not([returnType.typeAnnotation])` const selectors = [ `ClassDeclaration[superClass.name='ComponentStore'] CallExpression[callee.object.type='ThisExpression'][callee.property.name='updater'] > ${withoutTypeAnnotation}`, - storeName && - `${storeExpression( - storeName, + storeNames && + `${namedExpression( + storeNames, )}[callee.property.name='updater'] > ${withoutTypeAnnotation}`, ] .filter(Boolean) diff --git a/src/rules/effects/avoid-cyclic-effects.ts b/src/rules/effects/avoid-cyclic-effects.ts index c046d1a8..cefcbbed 100644 --- a/src/rules/effects/avoid-cyclic-effects.ts +++ b/src/rules/effects/avoid-cyclic-effects.ts @@ -4,9 +4,10 @@ import { getTypeServices } from 'eslint-etc' import path from 'path' import ts from 'typescript' import { + asPattern, createEffectExpression, docsUrl, - findNgRxEffectActionsName, + getNgRxEffectActions, isCallExpression, isIdentifier, isTypeReference, @@ -38,8 +39,12 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const actionsName = findNgRxEffectActionsName(context) - if (!actionsName) return {} + const { identifiers = [] } = getNgRxEffectActions(context) + const actionsNames = identifiers.length > 0 ? asPattern(identifiers) : null + + if (!actionsNames) { + return {} + } const { getType, typeChecker } = getTypeServices(context) @@ -145,7 +150,7 @@ export default ESLintUtils.RuleCreator(docsUrl)({ } return { - [`${createEffectExpression}:not([arguments.1]:has(Property[key.name='dispatch'][value.value=false])) CallExpression[callee.property.name='pipe'][callee.object.property.name='${actionsName}']`]: + [`${createEffectExpression}:not([arguments.1]:has(Property[key.name='dispatch'][value.value=false])) CallExpression[callee.property.name='pipe'][callee.object.property.name=${actionsNames}]`]: checkNode, } }, diff --git a/src/rules/effects/no-dispatch-in-effects.ts b/src/rules/effects/no-dispatch-in-effects.ts index bedb8d47..c3411cd8 100644 --- a/src/rules/effects/no-dispatch-in-effects.ts +++ b/src/rules/effects/no-dispatch-in-effects.ts @@ -2,9 +2,10 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, dispatchInEffects, docsUrl, - findNgRxStoreName, + getNgRxStores, isArrowFunctionExpression, isReturnStatement, } from '../../utils' @@ -39,11 +40,15 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers = [] } = getNgRxStores(context) + const storeNames = identifiers.length > 0 ? asPattern(identifiers) : null + + if (!storeNames) { + return {} + } return { - [dispatchInEffects(storeName)]( + [dispatchInEffects(storeNames)]( node: MemberExpressionWithinCallExpression, ) { const nodeToReport = getNodeToReport(node) diff --git a/src/rules/effects/prefer-concat-latest-from.ts b/src/rules/effects/prefer-concat-latest-from.ts index 5194ef72..2ddf9d6b 100644 --- a/src/rules/effects/prefer-concat-latest-from.ts +++ b/src/rules/effects/prefer-concat-latest-from.ts @@ -3,11 +3,12 @@ import { isArrowFunctionExpression } from 'eslint-etc' import path from 'path' import { createRule } from '../../rule-creator' import { + asPattern, createEffectExpression, - findNgRxEffectActionsName, getImportAddFix, + getNgRxEffectActions, + namedExpression, NGRX_MODULE_PATHS, - storeExpression, } from '../../utils' export const messageId = 'preferConcatLatestFrom' @@ -52,8 +53,7 @@ export default createRule({ }, defaultOptions: [defaultOptions], create: (context, [options]) => { - const sourceCode = context.getSourceCode() - const selector = getSelector(context, options) + const { selector, sourceCode } = getSelectorWithSourceCode(context, options) return { ...(selector && { @@ -69,23 +69,30 @@ export default createRule({ }, }) -function getSelector( +function getSelectorWithSourceCode( context: Readonly>, { strict }: Options[number], ) { if (strict) { - return `${createEffectExpression} CallExpression > Identifier[name='withLatestFrom']` as const + return { + selector: `${createEffectExpression} CallExpression > Identifier[name='withLatestFrom']`, + sourceCode: context.getSourceCode(), + } as const } - const actionsName = findNgRxEffectActionsName(context) + const { identifiers, sourceCode } = getNgRxEffectActions(context) + const actionsNames = identifiers?.length ? asPattern(identifiers) : null - if (!actionsName) { - return null + if (!actionsNames) { + return { sourceCode } } - return `${createEffectExpression} ${storeExpression( - actionsName, - )} > CallExpression[arguments.length=1] > Identifier[name='${withLatestFromKeyword}']` as const + return { + selector: `${createEffectExpression} ${namedExpression( + actionsNames, + )} > CallExpression[arguments.length=1] > Identifier[name='${withLatestFromKeyword}']`, + sourceCode, + } as const } function getFixes( diff --git a/src/rules/store/avoid-combining-selectors.ts b/src/rules/store/avoid-combining-selectors.ts index f9fc3d42..091a4a32 100644 --- a/src/rules/store/avoid-combining-selectors.ts +++ b/src/rules/store/avoid-combining-selectors.ts @@ -2,10 +2,11 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, docsUrl, - findNgRxStoreName, - storeExpression, - storeSelect, + getNgRxStores, + namedExpression, + selectExpression, } from '../../utils' export const messageId = 'avoidCombiningSelectors' @@ -31,13 +32,17 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers = [] } = getNgRxStores(context) + const storeNames = identifiers.length > 0 ? asPattern(identifiers) : null - const pipeableOrStoreSelect = `:matches(${storeExpression( - storeName, - )}[callee.property.name='pipe']:has(CallExpression[callee.name='select']), ${storeSelect( - storeName, + if (!storeNames) { + return {} + } + + const pipeableOrStoreSelect = `:matches(${namedExpression( + storeNames, + )}[callee.property.name='pipe']:has(CallExpression[callee.name='select']), ${selectExpression( + storeNames, )})` as const return { diff --git a/src/rules/store/avoid-dispatching-multiple-actions-sequentially.ts b/src/rules/store/avoid-dispatching-multiple-actions-sequentially.ts index 85ae087b..9616c7d3 100644 --- a/src/rules/store/avoid-dispatching-multiple-actions-sequentially.ts +++ b/src/rules/store/avoid-dispatching-multiple-actions-sequentially.ts @@ -1,7 +1,12 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' -import { docsUrl, findNgRxStoreName, storeDispatch } from '../../utils' +import { + asPattern, + dispatchExpression, + docsUrl, + getNgRxStores, +} from '../../utils' export const messageId = 'avoidDispatchingMultipleActionsSequentially' export type MessageIds = typeof messageId @@ -25,15 +30,19 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers = [] } = getNgRxStores(context) + const storeNames = identifiers.length > 0 ? asPattern(identifiers) : null + + if (!storeNames) { + return {} + } const collectedDispatches = new Set() return { - [`BlockStatement > ExpressionStatement > ${storeDispatch(storeName)}`]( - node: TSESTree.CallExpression, - ) { + [`BlockStatement > ExpressionStatement > ${dispatchExpression( + storeNames, + )}`](node: TSESTree.CallExpression) { collectedDispatches.add(node) }, 'BlockStatement:exit'() { diff --git a/src/rules/store/avoid-mapping-selectors.ts b/src/rules/store/avoid-mapping-selectors.ts index 2ce3a0c0..68c0eeb1 100644 --- a/src/rules/store/avoid-mapping-selectors.ts +++ b/src/rules/store/avoid-mapping-selectors.ts @@ -2,10 +2,11 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, docsUrl, - findNgRxStoreName, - storeExpressionCallable, - storePipe, + getNgRxStores, + namedCallableExpression, + pipeExpression, } from '../../utils' export const messageId = 'avoidMapppingSelectors' @@ -31,14 +32,18 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers = [] } = getNgRxStores(context) + const storeNames = identifiers.length > 0 ? asPattern(identifiers) : null - const pipeWithSelectAndMapSelector = `${storePipe( - storeName, + if (!storeNames) { + return {} + } + + const pipeWithSelectAndMapSelector = `${pipeExpression( + storeNames, )}:has(CallExpression[callee.name='select'] ~ CallExpression[callee.name='map'])` as const - const selectSelector = `${storeExpressionCallable( - storeName, + const selectSelector = `${namedCallableExpression( + storeNames, )}[callee.object.callee.property.name='select']` as const return { diff --git a/src/rules/store/no-multiple-global-stores.ts b/src/rules/store/no-multiple-global-stores.ts index af6e3975..c7397ebc 100644 --- a/src/rules/store/no-multiple-global-stores.ts +++ b/src/rules/store/no-multiple-global-stores.ts @@ -2,10 +2,9 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { - constructorExit, docsUrl, + getNgRxStores, getNodeToCommaRemoveFix, - injectedStore, isTSParameterProperty, } from '../../utils' @@ -35,38 +34,39 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const sourceCode = context.getSourceCode() - const collectedStores = new Set() - return { - [injectedStore](node: TSESTree.Identifier) { - collectedStores.add(node) - }, - [constructorExit]() { - const stores = [...collectedStores] - collectedStores.clear() + Program() { + const { identifiers = [], sourceCode } = getNgRxStores(context) + const flattenedIdentifiers = groupBy(identifiers).values() - if (stores.length <= 1) { - return - } + for (const identifiers of flattenedIdentifiers) { + if (identifiers.length <= 1) { + continue + } - for (const node of stores) { - context.report({ - node, - messageId: noMultipleGlobalStores, - suggest: [ - { - messageId: noMultipleGlobalStoresSuggest, - fix: (fixer) => getFixes(sourceCode, fixer, node), - }, - ], - }) + for (const node of identifiers) { + const nodeToReport = getNodeToReport(node) + context.report({ + node: nodeToReport, + messageId: noMultipleGlobalStores, + suggest: [ + { + messageId: noMultipleGlobalStoresSuggest, + fix: (fixer) => getFixes(sourceCode, fixer, nodeToReport), + }, + ], + }) + } } }, } }, }) +function getNodeToReport(node: TSESTree.Node) { + return node.parent && isTSParameterProperty(node.parent) ? node.parent : node +} + function getFixes( sourceCode: Readonly, fixer: TSESLint.RuleFixer, @@ -76,3 +76,21 @@ function getFixes( const nodeToRemove = parent && isTSParameterProperty(parent) ? parent : node return getNodeToCommaRemoveFix(sourceCode, fixer, nodeToRemove) } + +type Identifiers = NonNullable['identifiers']> + +function groupBy(identifiers: Identifiers): Map { + return identifiers.reduce>( + (accumulator, identifier) => { + const parent = isTSParameterProperty(identifier.parent) + ? identifier.parent.parent + : identifier.parent + const collectedIdentifiers = accumulator.get(parent) + return accumulator.set(parent, [ + ...(collectedIdentifiers ?? []), + identifier, + ]) + }, + new Map(), + ) +} diff --git a/src/rules/store/no-store-subscription.ts b/src/rules/store/no-store-subscription.ts index cb1b66fb..7df39e26 100644 --- a/src/rules/store/no-store-subscription.ts +++ b/src/rules/store/no-store-subscription.ts @@ -2,9 +2,10 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, docsUrl, - findNgRxStoreName, - storeExpressionCallable, + getNgRxStores, + namedCallableExpression, } from '../../utils' export const messageId = 'noStoreSubscription' @@ -30,12 +31,16 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers = [] } = getNgRxStores(context) + const storeNames = identifiers.length > 0 ? asPattern(identifiers) : null + + if (!storeNames) { + return {} + } return { - [`${storeExpressionCallable( - storeName, + [`${namedCallableExpression( + storeNames, )} > MemberExpression > Identifier[name='subscribe']`]( node: TSESTree.Identifier, ) { diff --git a/src/rules/store/no-typed-global-store.ts b/src/rules/store/no-typed-global-store.ts index 4215382e..12f098c5 100644 --- a/src/rules/store/no-typed-global-store.ts +++ b/src/rules/store/no-typed-global-store.ts @@ -1,7 +1,6 @@ -import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' -import { docsUrl, typedStore } from '../../utils' +import { docsUrl, getNgRxStores, isTSTypeReference } from '../../utils' export const noTypedStore = 'noTypedStore' export const noTypedStoreSuggest = 'noTypedStoreSuggest' @@ -29,17 +28,32 @@ export default ESLintUtils.RuleCreator(docsUrl)({ defaultOptions: [], create: (context) => { return { - [typedStore](node: TSESTree.TSTypeParameterInstantiation) { - context.report({ - node, - messageId: noTypedStore, - suggest: [ - { - messageId: noTypedStoreSuggest, - fix: (fixer) => fixer.remove(node), - }, - ], - }) + Program() { + const { identifiers = [] } = getNgRxStores(context) + + for (const { + typeAnnotation: { typeAnnotation }, + } of identifiers) { + if ( + !isTSTypeReference(typeAnnotation) || + !typeAnnotation.typeParameters + ) { + continue + } + + const { typeParameters } = typeAnnotation + + context.report({ + node: typeParameters, + messageId: noTypedStore, + suggest: [ + { + messageId: noTypedStoreSuggest, + fix: (fixer) => fixer.remove(typeParameters), + }, + ], + }) + } }, } }, diff --git a/src/rules/store/prefer-action-creator-in-dispatch.ts b/src/rules/store/prefer-action-creator-in-dispatch.ts index 9d2c4c95..f3e41758 100644 --- a/src/rules/store/prefer-action-creator-in-dispatch.ts +++ b/src/rules/store/prefer-action-creator-in-dispatch.ts @@ -2,12 +2,13 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, + dispatchExpression, docsUrl, - findNgRxStoreName, getNearestUpperNodeFrom, + getNgRxStores, isCallExpression, isCallExpressionWith, - storeDispatch, } from '../../utils' export const messageId = 'preferActionCreatorInDispatch' @@ -33,12 +34,16 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers = [] } = getNgRxStores(context) + const storeNames = identifiers.length > 0 ? asPattern(identifiers) : null + + if (!storeNames) { + return {} + } return { - [`${storeDispatch( - storeName, + [`${dispatchExpression( + storeNames, )} :matches(NewExpression, :not(NewExpression) > ObjectExpression)`]( node: TSESTree.NewExpression | TSESTree.ObjectExpression, ) { @@ -50,7 +55,7 @@ export default ESLintUtils.RuleCreator(docsUrl)({ nearestUpperCallExpression !== undefined && isCallExpressionWith( nearestUpperCallExpression, - storeName, + storeNames, 'dispatch', ) diff --git a/src/rules/store/select-style.ts b/src/rules/store/select-style.ts index 80549b5f..b17ea34e 100644 --- a/src/rules/store/select-style.ts +++ b/src/rules/store/select-style.ts @@ -2,17 +2,18 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, docsUrl, - findNgRxStoreName, getImportAddFix, getImportRemoveFix, getNearestUpperNodeFrom, + getNgRxStores, isCallExpression, isClassDeclaration, isMemberExpression, NGRX_MODULE_PATHS, pipeableSelect, - storeSelect, + selectExpression, } from '../../utils' export const methodSelectMessageId = 'methodSelect' @@ -69,12 +70,16 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [SelectStyle.Method], create: (context, [mode]) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers, sourceCode } = getNgRxStores(context) + const storeNames = identifiers?.length ? asPattern(identifiers) : null + + if (!storeNames) { + return {} + } if (mode === SelectStyle.Operator) { return { - [storeSelect(storeName)](node: CallExpression) { + [selectExpression(storeNames)](node: CallExpression) { context.report({ node: node.callee.property, messageId: operatorSelectMessageId, @@ -84,11 +89,9 @@ export default ESLintUtils.RuleCreator(docsUrl)({ } } - const sourceCode = context.getSourceCode() - return { [`Program:has(${pipeableSelect( - storeName, + storeNames, )}) ImportDeclaration[source.value='${ NGRX_MODULE_PATHS.store }'] > ImportSpecifier[imported.name='select']`]( diff --git a/src/rules/store/use-consistent-global-store-name.ts b/src/rules/store/use-consistent-global-store-name.ts index a6e5863c..af5b29ff 100644 --- a/src/rules/store/use-consistent-global-store-name.ts +++ b/src/rules/store/use-consistent-global-store-name.ts @@ -1,7 +1,6 @@ -import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' -import { docsUrl, injectedStore } from '../../utils' +import { docsUrl, getNgRxStores } from '../../utils' export const useConsistentGlobalStoreName = 'useConsistentGlobalStoreName' export const useConsistentGlobalStoreNameSuggest = @@ -37,37 +36,38 @@ export default ESLintUtils.RuleCreator(docsUrl)({ defaultOptions: ['store'], create: (context, [storeName]) => { return { - [`${injectedStore}[name!=${storeName}]`]({ - loc, - name, - range, - typeAnnotation, - }: TSESTree.Identifier & { - typeAnnotation: TSESTree.TSTypeAnnotation - }) { - const data = { storeName } - context.report({ - loc: { - ...loc, - end: { - ...loc.start, - column: loc.start.column + name.length, - }, - }, - messageId: useConsistentGlobalStoreName, - data, - suggest: [ - { - messageId: useConsistentGlobalStoreNameSuggest, - data, - fix: (fixer) => - fixer.replaceTextRange( - [range[0], typeAnnotation.range[0]], - storeName, - ), + Program() { + const { identifiers = [] } = getNgRxStores(context) + + for (const { loc, name, range, typeAnnotation } of identifiers) { + if (name === storeName) { + return + } + + const data = { storeName } + context.report({ + loc: { + ...loc, + end: { + ...loc.start, + column: loc.start.column + name.length, + }, }, - ], - }) + messageId: useConsistentGlobalStoreName, + data, + suggest: [ + { + messageId: useConsistentGlobalStoreNameSuggest, + data, + fix: (fixer) => + fixer.replaceTextRange( + [range[0], typeAnnotation.range[0]], + storeName, + ), + }, + ], + }) + } }, } }, diff --git a/src/rules/store/use-selector-in-select.ts b/src/rules/store/use-selector-in-select.ts index 3db71cd0..0beb4602 100644 --- a/src/rules/store/use-selector-in-select.ts +++ b/src/rules/store/use-selector-in-select.ts @@ -2,13 +2,14 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils' import { ESLintUtils } from '@typescript-eslint/experimental-utils' import path from 'path' import { + asPattern, docsUrl, - findNgRxStoreName, + getNgRxStores, isArrowFunctionExpression, isFunctionExpression, isLiteral, pipeableSelect, - storeSelect, + selectExpression, } from '../../utils' export const messageId = 'useSelectorInSelect' @@ -34,24 +35,28 @@ export default ESLintUtils.RuleCreator(docsUrl)({ }, defaultOptions: [], create: (context) => { - const storeName = findNgRxStoreName(context) - if (!storeName) return {} + const { identifiers = [] } = getNgRxStores(context) + const storeNames = identifiers.length > 0 ? asPattern(identifiers) : null + + if (!storeNames) { + return {} + } return { - [`${pipeableSelect(storeName)}, ${storeSelect(storeName)}`]( + [`${pipeableSelect(storeNames)}, ${selectExpression(storeNames)}`]( node: TSESTree.CallExpression, ) { - for (const arg of node.arguments) { + for (const argument of node.arguments) { if ( - !isLiteral(arg) && - !isArrowFunctionExpression(arg) && - !isFunctionExpression(arg) + !isLiteral(argument) && + !isArrowFunctionExpression(argument) && + !isFunctionExpression(argument) ) { break } context.report({ - node: arg, + node: argument, messageId, }) } diff --git a/src/utils/helper-functions/guards.ts b/src/utils/helper-functions/guards.ts index ac890097..704348f2 100644 --- a/src/utils/helper-functions/guards.ts +++ b/src/utils/helper-functions/guards.ts @@ -56,9 +56,13 @@ export function isTypeReference(type: ts.Type): type is ts.TypeReference { return type.hasOwnProperty('target') } +function equalTo(one: RegExp | string, other: string) { + return typeof one === 'string' ? one === other : one.test(other) +} + export function isCallExpressionWith( node: TSESTree.CallExpression, - objectName: string, + objectName: RegExp | string, propertyName: string, ) { return ( @@ -66,10 +70,10 @@ export function isCallExpressionWith( !node.callee.computed && node.callee.property.name === propertyName && ((isIdentifier(node.callee.object) && - node.callee.object.name === objectName) || + equalTo(objectName, node.callee.object.name)) || (isMemberExpression(node.callee.object) && isThisExpression(node.callee.object.object) && isIdentifier(node.callee.object.property) && - node.callee.object.property.name === objectName)) + equalTo(objectName, node.callee.object.property.name))) ) } diff --git a/src/utils/helper-functions/utils.ts b/src/utils/helper-functions/utils.ts index 696123bf..b6882972 100644 --- a/src/utils/helper-functions/utils.ts +++ b/src/utils/helper-functions/utils.ts @@ -20,6 +20,23 @@ import { } from './guards' import { NGRX_MODULE_PATHS } from './ngrx-modules' +type ConstructorFunctionExpression = TSESTree.FunctionExpression & { + parent: TSESTree.MethodDefinition & { kind: 'constructor' } +} +type InjectedParameter = + | TSESTree.Identifier & { + typeAnnotation: TSESTree.TSTypeAnnotation + parent: + | ConstructorFunctionExpression + | (TSESTree.TSParameterProperty & { + parent: ConstructorFunctionExpression + }) + } +type InjectedParameterWithSourceCode = Readonly<{ + identifiers?: readonly InjectedParameter[] + sourceCode: Readonly +}> + export function getNearestUpperNodeFrom( { parent }: TSESTree.Node, predicate: (parent: TSESTree.Node) => parent is T, @@ -67,10 +84,9 @@ export function getImportDeclarations( parentNode = parentNode.parent } - return parentNode?.body.filter( - (node): node is TSESTree.ImportDeclaration => - isImportDeclaration(node) && node.source.value === moduleName, - ) + return parentNode?.body.filter((node): node is TSESTree.ImportDeclaration => { + return isImportDeclaration(node) && node.source.value === moduleName + }) } function getCorrespondentImportClause( @@ -302,59 +318,78 @@ export function capitalize(text: T): Capitalize { return `${text[0].toUpperCase()}${text.slice(1)}` as Capitalize } -function findCorrespondingNameBy( +function getInjectedParametersWithSourceCode( context: TSESLint.RuleContext, moduleName: string, importName: string, -): string | undefined { - const { ast } = context.getSourceCode() - const importDeclarations = getImportDeclarations(ast, moduleName) ?? [] +): InjectedParameterWithSourceCode { + const sourceCode = context.getSourceCode() + const importDeclarations = + getImportDeclarations(sourceCode.ast, moduleName) ?? [] const { importSpecifier } = getImportDeclarationSpecifier(importDeclarations, importName) ?? {} if (!importSpecifier) { - return undefined + return { sourceCode } } const variables = context.getDeclaredVariables(importSpecifier) const typedVariable = variables.find(({ name }) => name === importName) + const identifiers = typedVariable?.references?.reduce< + readonly InjectedParameter[] + >((identifiers, { identifier: { parent } }) => { + if ( + parent && + isTSTypeReference(parent) && + parent.parent && + isTSTypeAnnotation(parent.parent) && + parent.parent.parent && + isIdentifier(parent.parent.parent) + ) { + return identifiers.concat(parent.parent.parent as InjectedParameter) + } - return typedVariable?.references - .map(({ identifier: { parent } }) => { - if ( - parent && - isTSTypeReference(parent) && - parent.parent && - isTSTypeAnnotation(parent.parent) && - parent.parent.parent && - isIdentifier(parent.parent.parent) - ) { - return parent.parent.parent.name - } - - return undefined - }) - .find(Boolean) + return identifiers + }, []) + return { identifiers, sourceCode } } -export function findNgRxStoreName( +export function getNgRxEffectActions( context: TSESLint.RuleContext, -): string | undefined { - return findCorrespondingNameBy(context, NGRX_MODULE_PATHS.store, 'Store') +): InjectedParameterWithSourceCode { + return getInjectedParametersWithSourceCode( + context, + NGRX_MODULE_PATHS.effects, + 'Actions', + ) } -export function findNgRxComponentStoreName( +export function getNgRxComponentStores( context: TSESLint.RuleContext, -): string | undefined { - return findCorrespondingNameBy( +): InjectedParameterWithSourceCode { + return getInjectedParametersWithSourceCode( context, NGRX_MODULE_PATHS['component-store'], 'ComponentStore', ) } -export function findNgRxEffectActionsName( +export function getNgRxStores( context: TSESLint.RuleContext, -): string | undefined { - return findCorrespondingNameBy(context, NGRX_MODULE_PATHS.effects, 'Actions') +): InjectedParameterWithSourceCode { + return getInjectedParametersWithSourceCode( + context, + NGRX_MODULE_PATHS.store, + 'Store', + ) +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping +export function escapeText(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +export function asPattern(identifiers: readonly InjectedParameter[]): RegExp { + const escapedNames = identifiers.map(({ name }) => escapeText(name)) + return new RegExp(`^(${escapedNames.join('|')})$`) } diff --git a/src/utils/selectors/index.ts b/src/utils/selectors/index.ts index 5e0dd8ab..4bcbfa79 100644 --- a/src/utils/selectors/index.ts +++ b/src/utils/selectors/index.ts @@ -13,7 +13,7 @@ export const actionCreatorProps = export const actionCreatorPropsComputed = `${actionCreatorProps} > TSTypeParameterInstantiation > :matches(TSTypeReference[typeName.name!='Readonly'], [type=/^TS(.*)(Keyword|Type)$/])` as const -export const constructorExit = `MethodDefinition[kind='constructor']:exit` +export const constructorDefinition = `MethodDefinition[kind='constructor']` export function metadataProperty(key: RegExp): string export function metadataProperty( @@ -23,9 +23,6 @@ export function metadataProperty(key: RegExp | string): string { return `Property:matches([key.name=${key}][computed=false], [key.value=${key}], [key.quasis.0.value.raw=${key}])` } -export const injectedStore = `MethodDefinition[kind='constructor'] Identifier[typeAnnotation.typeAnnotation.typeName.name='Store']` -export const typedStore = `MethodDefinition[kind='constructor'] Identifier > TSTypeAnnotation > TSTypeReference[typeName.name='Store'] > .typeParameters[params]` - export const ngModuleDecorator = `ClassDeclaration > Decorator > CallExpression[callee.name='NgModule']` export const ngModuleImports = @@ -44,28 +41,30 @@ export const effectsInNgModuleImports = export const effectsInNgModuleProviders = `${ngModuleProviders} Identifier` as const -export const storeExpression = (storeName: string) => - `CallExpression:matches([callee.object.name='${storeName}'], [callee.object.object.type='ThisExpression'][callee.object.property.name='${storeName}'])` as const +export const namedExpression = (name: RegExp | string) => + `:matches(${constructorDefinition} CallExpression[callee.object.name=${name}], CallExpression[callee.object.object.type='ThisExpression'][callee.object.property.name=${name}])` as const -export const storeExpressionCallable = (storeName: string) => - `CallExpression:matches([callee.object.callee.object.name='${storeName}'], [callee.object.callee.object.object.type='ThisExpression'][callee.object.callee.object.property.name='${storeName}'])` as const +export const namedCallableExpression = (name: RegExp | string) => + `:matches(${namedExpression( + name, + )}, ${constructorDefinition} CallExpression[callee.object.callee.object.name=${name}], CallExpression[callee.object.callee.object.object.type='ThisExpression'][callee.object.callee.object.property.name=${name}])` as const -export const storePipe = (storeName: string) => - `${storeExpression(storeName)}[callee.property.name='pipe']` as const +export const pipeExpression = (name: RegExp | string) => + `${namedExpression(name)}[callee.property.name='pipe']` as const -export const pipeableSelect = (storeName: string) => - `${storePipe(storeName)} CallExpression[callee.name='select']` as const +export const pipeableSelect = (name: RegExp | string) => + `${pipeExpression(name)} CallExpression[callee.name='select']` as const -export const storeSelect = (storeName: string) => - `${storeExpression(storeName)}[callee.property.name='select']` as const +export const selectExpression = (name: RegExp | string) => + `${namedExpression(name)}[callee.property.name='select']` as const -export const storeDispatch = (storeName: string) => - `${storeExpression(storeName)}[callee.property.name='dispatch']` as const +export const dispatchExpression = (name: RegExp | string) => + `${namedExpression(name)}[callee.property.name='dispatch']` as const -export const dispatchInEffects = (storeName: string) => - `${createEffectExpression} ${storeDispatch( - storeName, - )} > MemberExpression:has(Identifier[name='${storeName}'])` as const +export const dispatchInEffects = (name: RegExp | string) => + `${createEffectExpression} ${dispatchExpression( + name, + )} > MemberExpression:has(Identifier[name=${name}])` as const export const createReducer = `CallExpression[callee.name='createReducer']` diff --git a/tests/rules/avoid-combining-selectors.test.ts b/tests/rules/avoid-combining-selectors.test.ts index 6edeb921..c6548dc5 100644 --- a/tests/rules/avoid-combining-selectors.test.ts +++ b/tests/rules/avoid-combining-selectors.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { @@ -9,111 +8,141 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly test$ = somethingOutside(); - }`, +import { Store } from '@ngrx/store' + +class Ok { + readonly test$ = somethingOutside(); +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = this.store.select(selectItems) - constructor(private store: Store){} - } - `, +import { Store } from '@ngrx/store' + +class Ok1 { + readonly vm$: Observable + + constructor(store: Store) { + this.vm$ = store.select(selectItems) + } +}`, ` - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = this.store.pipe(select(selectItems)) - constructor(private store: Store){} - } - `, +import { Store, select } from '@ngrx/store' + +class Ok2 { + vm$ = this.store.pipe(select(selectItems)) + + constructor(private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.store.select(selectItems), this.somethingElse()) - constructor(private store: Store){} - } - `, +import { Store } from '@ngrx/store' + +class Ok3 { + vm$ = combineLatest(this.store$.select(selectItems), this.somethingElse()) + + constructor(private store$: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.somethingElse(), this.store.select(selectItems)) - constructor(private store: Store){} - } - `, +import { Store } from '@ngrx/store' + +@Pipe() +class Ok4 { + vm$ = combineLatest(this.somethingElse(), this.store.select(selectItems)) + + constructor(private readonly store: Store) {} +}`, ], invalid: [ fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.store.select(selectItems), this.store.select(selectOtherItems)) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - constructor(private store: Store){} - } - `, + ` +import { Store } from '@ngrx/store' + +class NotOk { + readonly vm$: Observable + + constructor(store: Store, private store2: Store) { + this.vm$ = combineLatest( + store.select(selectItems), + store.select(selectOtherItems), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.store2.select(selectOtherItems), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + ) + } +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.store.pipe(select(selectItems)), this.store.pipe(select(selectOtherItems))) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - constructor(private store: Store){} - } - `, + ` +import { Store } from '@ngrx/store' + +class NotOk1 { + vm$ = combineLatest( + this.store.pipe(select(selectItems)), + this.store.pipe(select(selectOtherItems)), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + ) + + constructor(private store: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.customName.select(selectItems), this.customName.select(selectOtherItems), this.somethingElse()) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - constructor(private customName: Store){} - } - `, + ` +import { Store } from '@ngrx/store' + +class NotOk2 { + vm$ = combineLatest( + this.customName.select(selectItems), + this.customName.select(selectOtherItems), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.somethingElse(), + ) + + constructor(private customName: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.customName.select(selectItems), this.customName.pipe(select(selectOtherItems)), this.somethingElse()) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - constructor(private customName: Store){} - } - `, + ` +import { Store } from '@ngrx/store' + +@Pipe() +class NotOk3 { + vm$ = combineLatest( + this.customName.select(selectItems), + this.customName.pipe(select(selectOtherItems)), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.somethingElse(), + ) + + constructor(private readonly customName: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.store.pipe(select(selectItems)), this.store.select(selectOtherItems)) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - constructor(private store: Store){} - } - `, + ` +import { Store } from '@ngrx/store' + +class NotOk4 { + vm$ = combineLatest( + this.store.pipe(select(selectItems)), + this.store.select(selectOtherItems), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + ) + + constructor(private store: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = combineLatest(this.store.select(selectItems), this.store.pipe(select(selectOtherItems))) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - constructor(private store: Store){} - } - `, + ` +import { Store } from '@ngrx/store' + +class NotOk5 { + vm$ = combineLatest( + this.store.select(selectItems), + this.store.pipe(select(selectOtherItems)), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.store2.pipe(select(selectOtherItems)), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + ) + + constructor(private store: Store, private store2: Store) {} +}`, ), ], }) diff --git a/tests/rules/avoid-dispatching-multiple-actions-sequentially.test.ts b/tests/rules/avoid-dispatching-multiple-actions-sequentially.test.ts index ce54f67d..7d2228dd 100644 --- a/tests/rules/avoid-dispatching-multiple-actions-sequentially.test.ts +++ b/tests/rules/avoid-dispatching-multiple-actions-sequentially.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { @@ -9,129 +8,114 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - ngOnInit() { - this.dispatch(UserActions.add()) - } - }`, +import { Store } from '@ngrx/store' + +class Ok { + ngOnInit() { + this.dispatch(UserActions.add()) + } +}`, ` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - constructor(private store: Store){} - ping() { - this.store.dispatch({ type: 'PING' }) - } - }`, +import { Store } from '@ngrx/store' + +class Ok1 { + constructor(private store: Store) {} + + ping() { + this.store.dispatch(GameActions.ping()) + } +}`, // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/47 ` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - valid = false; - constructor(private store: Store){} - - pingPong() { - if (this.valid) { - this.store.dispatch({ type: 'PING' }) - } else { - this.store.dispatch({ type: 'PONG' }) - } - } - }`, +import { Store } from '@ngrx/store' + +class Ok2 { + constructor(private store: Store) {} + + pingPong() { + if (condition) { + this.store.dispatch(GameActions.ping()) + } else { + this.store.dispatch(GameActions.pong()) + } + } +}`, // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/86 ` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - valid = false; - constructor(private store: Store){} - - ngOnInit() { - this.store.subscribe(() => { - this.store.dispatch({ type : 'one' }); - }); - this.store.subscribe(() => { - this.store.dispatch({ type : 'another-one' }); - }); - } - }`, +import { Store } from '@ngrx/store' + +class Ok3 { + constructor(private store: Store) {} + + ngOnInit() { + this.store.subscribe(() => { + this.store.dispatch(one()); + }); + this.store.subscribe(() => { + this.store.dispatch(anotherOne()); + }); + } +}`, ], invalid: [ - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - constructor(private store: Store){} - - pingPong() { - this.store.dispatch({ type: 'PING' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - this.store.dispatch({ type: 'PONG' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - }`, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - constructor(private store: Store){} - - pong() { - this.store.dispatch({ type: 'PING' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - this.ping(); - this.name = 'Bob' - this.store.dispatch({ type: 'PONG' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - }`, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - constructor(private store: Store){} - - pingPongPong() { - this.store.dispatch({ type: 'PING' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - this.store.dispatch({ type: 'PONG' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - this.store.dispatch({ type: 'PONG' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - }`, - ), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk { + constructor(private store: Store) {} + + pingPong() { + this.store.dispatch(GameActions.ping()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.store.dispatch(GameActions.pong()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(store: Store, private readonly store$: Store) { + store.dispatch(GameActions.ping()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.ping(); + this.name = 'Bob' + this.store$.dispatch(GameActions.pong()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor(private store: Store) {} + + pingPongPong() { + this.store.dispatch(GameActions.ping()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.store.dispatch(GameActions.pong()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.store.dispatch(GameActions.pong()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/44 - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - - @Component() - export class FixtureComponent { - constructor(private customName: Store){} - - pingPong() { - this.customName.dispatch({ type: 'PING' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - this.customName.dispatch({ type: 'PONG' }) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - }`, - ), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk3 { + constructor(private customName: Store) {} + + ngOnInit() { + customName.dispatch() + } + + pingPong() { + this.customName.dispatch(GameActions.ping()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.customName.dispatch(GameActions.pong()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), ], }) diff --git a/tests/rules/avoid-mapping-selectors.test.ts b/tests/rules/avoid-mapping-selectors.test.ts index 031990a4..25e7a31d 100644 --- a/tests/rules/avoid-mapping-selectors.test.ts +++ b/tests/rules/avoid-mapping-selectors.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { messageId } from '../../src/rules/store/avoid-mapping-selectors' @@ -7,154 +6,148 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly test$ = somethingOutside(); - }`, +import { Store } from '@ngrx/store' + +class Ok { + readonly test$ = somethingOutside(); +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select(selectItems) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok1 { + foo$ = this.store.select(selectItems) + + constructor(private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select(selectItems).pipe(filter(x => !!x)) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok2 { + foo$ = this.store.select(selectItems).pipe(filter(x => !!x)) + + constructor(private store: Store) {} +}`, ` - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.pipe(select(selectItems)) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok3 { + foo$ = this.store.pipe(select(selectItems)) + + constructor(private store: Store) {} +}`, ` - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.pipe(select(selectItems)).pipe(filter(x => !!x)) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok4 { + foo$ = this.store.pipe(select(selectItems)).pipe(filter(x => !!x)) + + constructor(private store: Store) {} +}`, ` - import { Store, select } from '@ngrx/store' - @Injectable() - export class FixtureEffect { - loginUserSuccess$ = createEffect(() => { - return this.actions$.pipe( - ofType(AuthActions.loginUserSuccess), - concatLatestFrom(action => this.store.select(startUrl)), - map(([action, url]) => AuthActions.setStartUrl({data: ''})), - ) - } - ) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok5 { + loginUserSuccess$ = createEffect(() => { + return this.actions$.pipe( + ofType(AuthActions.loginUserSuccess), + concatLatestFrom(action => this.store.select(startUrl)), + map(([action, url]) => AuthActions.setStartUrl({ data: '' })), + ) + } + ) + + constructor(private store: Store) {} +}`, // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/174 ` - import { Store, select } from '@ngrx/store' - @Injectable() - export class FixtureEffect { - loginUserSuccess$ = combineLatest([ - this.store.select(selectAuthorizations), this.hasAuthorization$ - ]).pipe(map((val) => !isEmpty(intersection(val)))) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok6 { + loginUserSuccess$ = combineLatest([ + this.store.select(selectAuthorizations), this.hasAuthorization$ + ]).pipe(map((val) => !isEmpty(intersection(val)))) + + constructor(private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly customers$ = this.store$.select(({ customers }) => customers).pipe() - readonly users$ = this.store$ - .select('users') - .pipe(switchMap(() => of(items.map(parseItem)))) - - constructor(private readonly store$: Store) {} - - ngOnInit() { - this.store$.pipe() - } - } - `, +import { Store } from '@ngrx/store' + +class Ok7 { + readonly customers$ = this.store$.select(({ customers }) => customers).pipe() + readonly users$ = this.store$ + .select('users') + .pipe(switchMap(() => of(items.map(parseItem)))) + + constructor(private readonly store$: Store) {} + + ngOnInit() { + this.store$.pipe() + } +} +`, ], invalid: [ - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = this.store - .select(selectItems) - .pipe(map((item) => item.select())) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - - constructor(private store: Store) {} - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = this.customStore.select(selectItems).pipe( - filter((x) => !!x), - map(getName), - ~~~~~~~~~~~~ [${messageId}] - ) - - constructor(private customStore: Store) {} - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly vm$ = this.store.pipe( - select(selectItems), - map((item) => ({ name: item.name, ...item.pipe() })), - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - ) - - constructor(private readonly store: Store) {} - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly vm$: Observable - - constructor(store$: Store) { - this.vm$ = store$.pipe( - select(selectItems), - filter(Boolean), - map(({ name }) => ({ name })), - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - ) - } - } - `, - ), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk { + vm$ = this.store + .select(selectItems) + .pipe(map((item) => item.select())) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + + constructor(private store: Store) {} +} +`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk1 { + vm$ = this.customStore.select(selectItems).pipe( + filter((x) => !!x), + map(getName), + ~~~~~~~~~~~~ [${messageId}] + ) + + constructor(private customStore: Store) {} +} +`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk2 { + readonly vm$ = this.store.pipe( + select(selectItems), + map((item) => ({ name: item.name, ...item.pipe() })), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + ) + + constructor(private readonly store: Store) {} +} +`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk3 { + readonly test$: Observable + readonly vm$: Observable + + constructor(store$: Store, private readonly store: Store) { + this.vm$ = store$.pipe( + select(selectItems), + filter(Boolean), + map(({ name }) => ({ name })), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + ) + } + + buildSomething() { + this.test$ = this.store + .select(selectItems) + .pipe(map((item) => item.select())) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + } +} +`), ], }) diff --git a/tests/rules/no-dispatch-in-effects.test.ts b/tests/rules/no-dispatch-in-effects.test.ts index 7285f81b..838a4b49 100644 --- a/tests/rules/no-dispatch-in-effects.test.ts +++ b/tests/rules/no-dispatch-in-effects.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { @@ -10,210 +9,270 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly test$ = somethingOutside(); - }`, +import { Store } from '@ngrx/store' + +class Ok { + readonly effect = somethingOutside(); +}`, ` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - effectOK = createEffect(() => this.actions.pipe( - ofType('PING'), - tap(() => ({ type: 'PONG' })) - )) - - constructor(private actions: Actions, private store: Store) {} - }`, +import { Store } from '@ngrx/store' + +class Ok1 { + effect = createEffect(() => this.actions.pipe( + ofType('PING'), + tap(() => ({ type: 'PONG' })) + )) + + constructor(private actions: Actions, private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - readonly effectOK1: CreateEffectMetadata - - constructor(private actions: Actions, private store$: Store) { - this.effectOK1 = createEffect( - () => ({ scheduler = asyncScheduler } = {}) => - this.actions.pipe( - ofType(customerActions.remove), - tap(() => { - customObject.dispatch({ somethingElse: true }) - return customerActions.removeSuccess() - }), - ), - { dispatch: false }, - ) - } - }`, +import { Store } from '@ngrx/store' + +class Ok2 { + readonly effect: CreateEffectMetadata + + constructor(private actions: Actions, private store$: Store) { + this.effect = createEffect( + () => ({ scheduler = asyncScheduler } = {}) => + this.actions.pipe( + ofType(customerActions.remove), + tap(() => { + customObject.dispatch({ somethingElse: true }) + return customerActions.removeSuccess() + }), + ), + { dispatch: false }, + ) + } +}`, ], invalid: [ fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - effectNOK = createEffect( - () => { - return this.actions.pipe( - ofType(someAction), - tap(() => this.store.dispatch(awesomeAction())), - ~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects}] - ) - }, - { dispatch: false }, - ) + ` +import { Store } from '@ngrx/store' + +class NotOk { + effect = createEffect( + () => { + return this.actions.pipe( + ofType(someAction), + tap(() => this.store.dispatch(awesomeAction())), + ~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects}] + ) + }, + { dispatch: false }, + ) - constructor(private actions: Actions, private store: Store) {} - }`, + constructor(private actions: Actions, private store: Store) {} +}`, { suggestions: [ { messageId: noDispatchInEffectsSuggest, - output: stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - effectNOK = createEffect( - () => { - return this.actions.pipe( - ofType(someAction), - tap(() => (awesomeAction())), - ) - }, - { dispatch: false }, - ) - - constructor(private actions: Actions, private store: Store) {} - }`, + output: ` +import { Store } from '@ngrx/store' + +class NotOk { + effect = createEffect( + () => { + return this.actions.pipe( + ofType(someAction), + tap(() => (awesomeAction())), + ) + }, + { dispatch: false }, + ) + + constructor(private actions: Actions, private store: Store) {} +}`, }, ], }, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - effectNOK1 = createEffect(() => condition ? this.actions.pipe( - ofType(userActions.add), - tap(() => { - return this.store.dispatch(userActions.addSuccess) - ~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects}] - }) - ) : this.actions.pipe()) + ` +import { Store } from '@ngrx/store' + +class NotOk1 { + readonly effect = createEffect(() => condition ? this.actions.pipe( + ofType(userActions.add), + tap(() => { + return this.store.dispatch(userActions.addSuccess) + ~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects}] + }) + ) : this.actions.pipe()) - constructor(private actions: Actions, private store: Store) {} - }`, + constructor(private actions: Actions, private store: Store) {} +}`, { suggestions: [ { messageId: noDispatchInEffectsSuggest, - output: stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - effectNOK1 = createEffect(() => condition ? this.actions.pipe( - ofType(userActions.add), - tap(() => { - return (userActions.addSuccess) - }) - ) : this.actions.pipe()) - - constructor(private actions: Actions, private store: Store) {} - }`, + output: ` +import { Store } from '@ngrx/store' + +class NotOk1 { + readonly effect = createEffect(() => condition ? this.actions.pipe( + ofType(userActions.add), + tap(() => { + return (userActions.addSuccess) + }) + ) : this.actions.pipe()) + + constructor(private actions: Actions, private store: Store) {} +}`, }, ], }, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - effectNOK2 = createEffect( - () => ({ debounce = 200 } = {}) => - this.actions.pipe( - ofType(actions.ping), - tap(() => { - return this.customName.dispatch(/* you shouldn't do this */ actions.pong()) - ~~~~~~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects}] - }), - ), - ) - - constructor(private actions: Actions, private customName: Store) {} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk2 { + effect = createEffect( + () => ({ debounce = 200 } = {}) => + this.actions.pipe( + ofType(actions.ping), + tap(() => { + return this.customName.dispatch(/* you shouldn't do this */ actions.pong()) + ~~~~~~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects}] + }), + ), + ) + + constructor(private actions: Actions, private customName: Store) {} +}`, { suggestions: [ { messageId: noDispatchInEffectsSuggest, - output: stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - effectNOK2 = createEffect( - () => ({ debounce = 200 } = {}) => - this.actions.pipe( - ofType(actions.ping), - tap(() => { - return (/* you shouldn't do this */ actions.pong()) - }), - ), - ) - - constructor(private actions: Actions, private customName: Store) {} - }`, + output: ` +import { Store } from '@ngrx/store' + +class NotOk2 { + effect = createEffect( + () => ({ debounce = 200 } = {}) => + this.actions.pipe( + ofType(actions.ping), + tap(() => { + return (/* you shouldn't do this */ actions.pong()) + }), + ), + ) + + constructor(private actions: Actions, private customName: Store) {} +}`, }, ], }, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - readonly effectNOK3: CreateEffectMetadata - - constructor(private actions: Actions, private readonly store$: Store) { - this.effectNOK3 = createEffect( - () => - this.actions.pipe( - ofType(bookActions.load), - map(() => { - this.store$.dispatch(bookActions.loadSuccess());// you shouldn't do this - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects}] - return somethingElse() - }), - ), - { dispatch: true, useEffectsErrorHandler: false, ...options }, - ) - } - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk3 { + readonly effect : CreateEffectMetadata + readonly effect : CreateEffectMetadata + + constructor(private actions: Actions, store: Store, private readonly store$: Store) { + this.effect = createEffect( + () => + this.actions.pipe( + ofType(bookActions.load), + map(() => { + this.store$.dispatch(bookActions.loadSuccess());// you shouldn't do this + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${noDispatchInEffects} suggest 0] + return somethingElse() + }), + ), + { dispatch: true, useEffectsErrorHandler: false, ...options }, + ) + this.effect = createEffect( + () => + this.actions.pipe( + ofType(bookActions.load), + tap(() => store.dispatch(bookActions.loadSuccess())) + ~~~~~~~~~~~~~~ [${noDispatchInEffects} suggest 1] + ), + ) + } + + ngOnDestroy() { + store.dispatch() + } +}`, { suggestions: [ { messageId: noDispatchInEffectsSuggest, - output: stripIndent` - import { Store } from '@ngrx/store' - @Injectable() - export class FixtureEffects { - readonly effectNOK3: CreateEffectMetadata - - constructor(private actions: Actions, private readonly store$: Store) { - this.effectNOK3 = createEffect( - () => - this.actions.pipe( - ofType(bookActions.load), - map(() => { - ;// you shouldn't do this - return somethingElse() - }), - ), - { dispatch: true, useEffectsErrorHandler: false, ...options }, - ) - } - }`, + output: ` +import { Store } from '@ngrx/store' + +class NotOk3 { + readonly effect : CreateEffectMetadata + readonly effect : CreateEffectMetadata + + constructor(private actions: Actions, store: Store, private readonly store$: Store) { + this.effect = createEffect( + () => + this.actions.pipe( + ofType(bookActions.load), + map(() => { + ;// you shouldn't do this + return somethingElse() + }), + ), + { dispatch: true, useEffectsErrorHandler: false, ...options }, + ) + this.effect = createEffect( + () => + this.actions.pipe( + ofType(bookActions.load), + tap(() => store.dispatch(bookActions.loadSuccess())) + ), + ) + } + + ngOnDestroy() { + store.dispatch() + } +}`, + }, + { + messageId: noDispatchInEffectsSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk3 { + readonly effect : CreateEffectMetadata + readonly effect : CreateEffectMetadata + + constructor(private actions: Actions, store: Store, private readonly store$: Store) { + this.effect = createEffect( + () => + this.actions.pipe( + ofType(bookActions.load), + map(() => { + this.store$.dispatch(bookActions.loadSuccess());// you shouldn't do this + return somethingElse() + }), + ), + { dispatch: true, useEffectsErrorHandler: false, ...options }, + ) + this.effect = createEffect( + () => + this.actions.pipe( + ofType(bookActions.load), + tap(() => (bookActions.loadSuccess())) + ), + ) + } + + ngOnDestroy() { + store.dispatch() + } +}`, }, ], }, diff --git a/tests/rules/no-multiple-global-stores.test.ts b/tests/rules/no-multiple-global-stores.test.ts index 15bbdeb7..61c86db4 100644 --- a/tests/rules/no-multiple-global-stores.test.ts +++ b/tests/rules/no-multiple-global-stores.test.ts @@ -1,4 +1,4 @@ -import { stripIndents } from 'common-tags' +import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { noMultipleGlobalStores, @@ -8,177 +8,203 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ - `export class NoCtorOK {}`, ` - export class EmptyOk { - constructor() {} - }`, +class Ok {}`, ` - export class OneWithVisibilityOk { - constructor(private store: Store) {} - }`, +class Ok1 { + constructor() {} +}`, ` - export class OneWithoutVisibilityOk { - constructor(store: Store) {} - }`, +import { Store } from '@ngrx/store' + +class Ok2 { + constructor(private store: Store) {} +}`, + ` +import { Store } from '@ngrx/store' + +class Ok3 { + constructor(store: Store) {} +}`, ` - export class OnePlusExtraOk { - constructor(private store: Store, data: Service) {} - }`, +import { Store } from '@ngrx/store' + +class Ok4 { + constructor(private store: Store, data: Service) {} +}`, + ` +import { Store } from '@ngrx/store' + +class Ok5 { + constructor(private store: Store) {} +} + +class Ok6 { + constructor(private readonly store: Store) {} +}`, + ` +import { Store } from '@ngrx/store' + +class Ok7 { + constructor(store: Store, apiService: ApiService) {} +} + +class Ok8 { + constructor(public store$: Store) {} +}`, ], invalid: [ - { - code: stripIndents` - export class NotOk1 { - constructor(store: Store, store2: Store) {} - }`, - errors: [ - { - column: 13, - endColumn: 25, - line: 2, - messageId: noMultipleGlobalStores, - suggestions: [ - { - messageId: noMultipleGlobalStoresSuggest, - output: stripIndents` - export class NotOk1 { - constructor( store2: Store) {} - }`, - }, - ], - }, - { - column: 27, - endColumn: 40, - line: 2, - messageId: noMultipleGlobalStores, - suggestions: [ - { - messageId: noMultipleGlobalStoresSuggest, - output: stripIndents` - export class NotOk1 { - constructor(store: Store, ) {} - }`, - }, - ], - }, - ], - }, - { - code: stripIndents` - export class NotOk2 { - constructor(store: Store /* first store */, private readonly actions$: Actions, private store2: Store, b: B) {} - }`, - errors: [ - { - column: 13, - endColumn: 25, - line: 2, - messageId: noMultipleGlobalStores, - suggestions: [ - { - messageId: noMultipleGlobalStoresSuggest, - output: stripIndents` - export class NotOk2 { - constructor( /* first store */ private readonly actions$: Actions, private store2: Store, b: B) {} - }`, - }, - ], - }, - { - column: 89, - endColumn: 102, - line: 2, - messageId: noMultipleGlobalStores, - suggestions: [ - { - messageId: noMultipleGlobalStoresSuggest, - output: stripIndents` - export class NotOk2 { - constructor(store: Store /* first store */, private readonly actions$: Actions, b: B) {} - }`, - }, - ], - }, - ], - }, - { - code: stripIndents` - export class NotOk3 { - constructor( - a: A, - store: Store,// a comment - private readonly actions$: Actions, - private store2: Store, - private readonly store3: Store, - ) {} - }`, - errors: [ - { - column: 1, - endColumn: 13, - line: 4, - messageId: noMultipleGlobalStores, - suggestions: [ - { - messageId: noMultipleGlobalStoresSuggest, - output: stripIndents` - export class NotOk3 { - constructor( - a: A, - // a comment - private readonly actions$: Actions, - private store2: Store, - private readonly store3: Store, - ) {} - }`, - }, - ], - }, - { - column: 9, - endColumn: 22, - line: 6, - messageId: noMultipleGlobalStores, - suggestions: [ - { - messageId: noMultipleGlobalStoresSuggest, - output: stripIndents` - export class NotOk3 { - constructor( - a: A, - store: Store,// a comment - private readonly actions$: Actions, - - private readonly store3: Store, - ) {} - }`, - }, - ], - }, - { - column: 18, - endColumn: 31, - line: 7, - messageId: noMultipleGlobalStores, - suggestions: [ - { - messageId: noMultipleGlobalStoresSuggest, - output: stripIndents` - export class NotOk3 { - constructor( - a: A, - store: Store,// a comment - private readonly actions$: Actions, - private store2: Store, - - ) {} - }`, - }, - ], - }, - ], - }, + fromFixture( + ` +import { Store } from '@ngrx/store' + +class ShouldNotBreakLaterReports { + constructor(store: Store, apiService: ApiService) {} +} + +class ShouldNotBreakLaterReports1 { + constructor(public store$: Store) {} +} + +class NotOk { + constructor(store: Store, store2: Store) {} + ~~~~~~~~~~~~ [${noMultipleGlobalStores} suggest 0] + ~~~~~~~~~~~~~ [${noMultipleGlobalStores} suggest 1] +}`, + { + suggestions: [ + { + messageId: noMultipleGlobalStoresSuggest, + output: ` +import { Store } from '@ngrx/store' + +class ShouldNotBreakLaterReports { + constructor(store: Store, apiService: ApiService) {} +} + +class ShouldNotBreakLaterReports1 { + constructor(public store$: Store) {} +} + +class NotOk { + constructor( store2: Store) {} +}`, + }, + { + messageId: noMultipleGlobalStoresSuggest, + output: ` +import { Store } from '@ngrx/store' + +class ShouldNotBreakLaterReports { + constructor(store: Store, apiService: ApiService) {} +} + +class ShouldNotBreakLaterReports1 { + constructor(public store$: Store) {} +} + +class NotOk { + constructor(store: Store, ) {} +}`, + }, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(store: Store /* first store */, private readonly actions$: Actions, private store2: Store, b: B) {} + ~~~~~~~~~~~~ [${noMultipleGlobalStores} suggest 0] + ~~~~~~~~~~~~~~~~~~~~~ [${noMultipleGlobalStores} suggest 1] +}`, + { + suggestions: [ + { + messageId: noMultipleGlobalStoresSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor( /* first store */ private readonly actions$: Actions, private store2: Store, b: B) {} +}`, + }, + { + messageId: noMultipleGlobalStoresSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(store: Store /* first store */, private readonly actions$: Actions, b: B) {} +}`, + }, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor( + a: A, + store: Store,// a comment + ~~~~~~~~~~~~ [${noMultipleGlobalStores} suggest 0] + private readonly actions$: Actions, + private store2: Store, + ~~~~~~~~~~~~~~~~~~~~~ [${noMultipleGlobalStores} suggest 1] + private readonly store3: Store, + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${noMultipleGlobalStores} suggest 2] + ) {} +}`, + { + suggestions: [ + { + messageId: noMultipleGlobalStoresSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor( + a: A, + // a comment + private readonly actions$: Actions, + private store2: Store, + private readonly store3: Store, + ) {} +}`, + }, + { + messageId: noMultipleGlobalStoresSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor( + a: A, + store: Store,// a comment + private readonly actions$: Actions, + \n private readonly store3: Store, + ) {} +}`, + }, + { + messageId: noMultipleGlobalStoresSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor( + a: A, + store: Store,// a comment + private readonly actions$: Actions, + private store2: Store, + \n ) {} +}`, + }, + ], + }, + ), ], }) diff --git a/tests/rules/no-store-subscription.test.ts b/tests/rules/no-store-subscription.test.ts index f89089bf..9ed1e5b0 100644 --- a/tests/rules/no-store-subscription.test.ts +++ b/tests/rules/no-store-subscription.test.ts @@ -1,4 +1,4 @@ -import { stripIndent } from 'common-tags' +import {} from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { messageId } from '../../src/rules/store/no-store-subscription' @@ -7,141 +7,148 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly test$ = somethingOutside(); - }`, +import { Store } from '@ngrx/store' + +class Ok { + readonly test$ = somethingOutside(); +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = this.store.select(selectItems) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok1 { + vm$ = this.store.select(selectItems) + + constructor(private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - vm$ = this.store.pipe(select(selectItems)) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok2 { + vm$ = this.store.pipe(select(selectItems)) + + constructor(private store: Store) {} +}`, // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/175 ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly formControlChangesSubscription$ = this.formCtrlOrg.valueChanges - .pipe(takeUntil(this.drop)) - .subscribe((orgName) => { - this.store.dispatch(UserPageActions.checkOrgNameInput({ orgName })) - }) - - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok3 { + readonly formControlChangesSubscription$ = this.formCtrlOrg.valueChanges + .pipe(takeUntil(this.drop)) + .subscribe((orgName) => { + this.store.dispatch(UserPageActions.checkOrgNameInput({ orgName })) + }) + + constructor(private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly items$: Observable - readonly metrics$: Observable - - constructor(store: Store) { - this.items$ = store.pipe(select(selectItems)) - this.metrics$ = store.select(selectMetrics) - } - } - `, +import { Store } from '@ngrx/store' + +class Ok4 { + readonly items$: Observable + readonly metrics$: Observable + + constructor(store: Store) { + this.items$ = store.pipe(select(selectItems)) + this.metrics$ = store.select(selectMetrics) + } +}`, ], invalid: [ - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - sub = this.store.select(selectItems).subscribe() - ~~~~~~~~~ [${messageId}] - constructor(private store: Store) {} - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - sub = this.store.pipe(select(selectItems)).subscribe() - ~~~~~~~~~ [${messageId}] - constructor(private store: Store) {} - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private store: Store) {} - - ngOnInit() { - this.store.select(selectItems).subscribe() - ~~~~~~~~~ [${messageId}] - } - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - items: readonly Item[] - - constructor(private store: Store) {} - - ngOnInit() { - this.store.pipe(select(selectItems)).subscribe((items) => { - ~~~~~~~~~ [${messageId}] - this.items = items - }) - } - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly items: readonly Item[] - - constructor(store: Store) { - store.pipe(select(selectItems)).subscribe((items) => this.items = items) - ~~~~~~~~~ [${messageId}] - } - } - `, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly control = new FormControl() - - constructor(store: Store) { - this.control.valueChanges.subscribe(() => { - store.pipe(select(selectItems)).subscribe() - ~~~~~~~~~ [${messageId}] - }) - } - } - `, - ), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk { + constructor(store: Store) { + store.subscribe() + ~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(store: Store) { + store.pipe(map(selectItems)).subscribe() + ~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk2 { + sub = this.store.subscribe() + ~~~~~~~~~ [${messageId}] + + constructor(private store: Store) {} +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk3 { + sub = this.store.select(selectItems).subscribe() + ~~~~~~~~~ [${messageId}] + + constructor(private store: Store) {} +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk4 { + sub = this.store.pipe(select(selectItems)).subscribe() + ~~~~~~~~~ [${messageId}] + + constructor(private store: Store) {} +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk5 { + constructor(private store: Store) {} + + ngOnInit() { + this.store.select(selectItems).subscribe() + ~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk6 { + items: readonly Item[] + + constructor(private store: Store) {} + + ngOnInit() { + this.store.pipe(select(selectItems)).subscribe((items) => { + ~~~~~~~~~ [${messageId}] + this.items = items + }) + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk6 { + readonly items: readonly Item[] + + constructor(store: Store) { + store.pipe(select(selectItems)).subscribe((items) => this.items = items) + ~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk7 { + readonly control = new FormControl() + + constructor(store: Store) { + this.control.valueChanges.subscribe(() => { + store.pipe(select(selectItems)).subscribe() + ~~~~~~~~~ [${messageId}] + }) + } +}`), ], }) diff --git a/tests/rules/no-typed-global-store.test.ts b/tests/rules/no-typed-global-store.test.ts index 6072bab2..f521643f 100644 --- a/tests/rules/no-typed-global-store.test.ts +++ b/tests/rules/no-typed-global-store.test.ts @@ -1,4 +1,4 @@ -import { stripIndent } from 'common-tags' +import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { noTypedStore, @@ -8,126 +8,111 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ - stripIndent` - export class Ok { - constructor(store: Store) {} - }`, + ` +import { Store } from '@ngrx/store' + +export class Ok { + constructor(store: Store) {} +}`, ], invalid: [ - { - code: stripIndent` - export class NotOk1 { - constructor(store: Store) {} - }`, - errors: [ - { - column: 27, - endColumn: 41, - line: 2, - messageId: noTypedStore, - suggestions: [ - { - messageId: noTypedStoreSuggest, - output: stripIndent` - export class NotOk1 { - constructor(store: Store) {} - }`, - }, - ], - }, - ], - }, - { - code: stripIndent` - export class NotOk2 { - constructor(cdr: ChangeDetectionRef, private store: Store) {} - }`, - errors: [ - { - column: 60, - endColumn: 76, - line: 2, - messageId: noTypedStore, - suggestions: [ - { - messageId: noTypedStoreSuggest, - output: stripIndent` - export class NotOk2 { - constructor(cdr: ChangeDetectionRef, private store: Store) {} - }`, - }, - ], - }, - ], - }, - { - code: stripIndent` - export class NotOk3 { - constructor(private store: Store, private personsService: PersonsService) {} - }`, - errors: [ - { - column: 35, - endColumn: 40, - line: 2, - messageId: noTypedStore, - suggestions: [ - { - messageId: noTypedStoreSuggest, - output: stripIndent` - export class NotOk3 { - constructor(private store: Store, private personsService: PersonsService) {} - }`, - }, - ], - }, - ], - }, - { - code: stripIndent` - export class NotOk4 { - constructor(store: Store<{}>) {} - }`, - errors: [ - { - column: 27, - endColumn: 31, - line: 2, - messageId: noTypedStore, - suggestions: [ - { - messageId: noTypedStoreSuggest, - output: stripIndent` - export class NotOk4 { - constructor(store: Store) {} - }`, - }, - ], - }, - ], - }, - { - code: stripIndent` - export class NotOk5 { - constructor(store: Store) {} - }`, - errors: [ - { - column: 27, - endColumn: 35, - line: 2, - messageId: noTypedStore, - suggestions: [ - { - messageId: noTypedStoreSuggest, - output: stripIndent` - export class NotOk5 { - constructor(store: Store) {} - }`, - }, - ], - }, - ], - }, + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk { + constructor(store: Store) {} + ~~~~~~~~~~~~~~ [${noTypedStore}] +}`, + { + suggestions: [ + { + messageId: noTypedStoreSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk { + constructor(store: Store) {} +}`, + }, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(cdr: ChangeDetectorRef, private store: Store) {} + ~~~~~~~~~~~~~~~~ [${noTypedStore}] +}`, + { + suggestions: [ + { + messageId: noTypedStoreSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(cdr: ChangeDetectorRef, private store: Store) {} +}`, + }, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor(private readonly store: Store, private personsService: PersonsService) {} + ~~~~~ [${noTypedStore}] +}`, + { + suggestions: [ + { + messageId: noTypedStoreSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor(private readonly store: Store, private personsService: PersonsService) {} +}`, + }, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk3 { + constructor(store: Store<{}>, private customStore: Store) {} + ~~~~ [${noTypedStore} suggest 0] + ~~~~~~~~ [${noTypedStore} suggest 1] +}`, + { + suggestions: [ + { + messageId: noTypedStoreSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk3 { + constructor(store: Store, private customStore: Store) {} +}`, + }, + { + messageId: noTypedStoreSuggest, + output: ` +import { Store } from '@ngrx/store' + +class NotOk3 { + constructor(store: Store<{}>, private customStore: Store) {} +}`, + }, + ], + }, + ), ], }) diff --git a/tests/rules/prefer-action-creator-in-dispatch.test.ts b/tests/rules/prefer-action-creator-in-dispatch.test.ts index dbd0c48e..878bc0d1 100644 --- a/tests/rules/prefer-action-creator-in-dispatch.test.ts +++ b/tests/rules/prefer-action-creator-in-dispatch.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { @@ -9,90 +8,86 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - @Component() - class Test { - readonly viewModel$ = somethingElse() +class Ok { + readonly viewModel$ = somethingElse() - constructor(private readonly appFacade: AppFacade) {} - }`, + constructor(private readonly appFacade: AppFacade) {} +}`, ` - import { Store } from '@ngrx/store'; - @Directive() - class Test { - constructor(store$: Store) { - store$.dispatch(action) - } - }`, +import { Store } from '@ngrx/store' + +class Ok1 { + constructor(store$: Store) { + store$.dispatch(action) + } +}`, ` - import { Store } from '@ngrx/store'; - @Component() - class Test { - constructor(private readonly store$: Store) { - this.store$.dispatch(BookActions.load()) - } - }`, +import { Store } from '@ngrx/store' + +class Ok2 { + constructor(private readonly store$: Store) { + this.store$.dispatch(BookActions.load()) + } +}`, // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/255 ` - import { Store } from '@ngrx/store'; - @Component() - class Test { - constructor(private readonly store: Store) { - this.store.dispatch(login({ payload })); - this.store.dispatch(AuthActions.dispatch({ type: 'SUCCESS' })); - nonStore.dispatch(AuthActions.dispatch({ type: 'FAIL' })); - } - }`, +import { Store } from '@ngrx/store' + +class Ok3 { + constructor(private readonly store: Store) { + this.store.dispatch(login({ payload })); + this.store.dispatch(AuthActions.dispatch({ type: 'SUCCESS' })); + nonStore.dispatch(AuthActions.dispatch({ type: 'FAIL' })); + } +}`, ], invalid: [ - fromFixture( - stripIndent` - import { Store } from '@ngrx/store'; - @Component() - class Test { - constructor(store$: Store) { - store$.dispatch(new CustomAction()) - ~~~~~~~~~~~~~~~~~~ [${messageId}] - } - }`, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store'; - @Injectable() - class Test { - constructor(private readonly store$: Store) { - this.store$.dispatch({ type: 'custom' }) - ~~~~~~~~~~~~~~~~~~ [${messageId}] - } - }`, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store'; - @Injectable() - class Test { - constructor(private readonly store$: Store) { - this.store$.dispatch(new Login({ payload })); - ~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - this.store$.dispatch(new AuthActions.dispatch({ type: 'SUCCESS' })); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - nonStore.dispatch(new AuthActions.dispatch({ type: 'FAIL' })); - } - }`, - ), - fromFixture( - stripIndent` - import { Store } from '@ngrx/store'; - @Component() - class Test { - constructor(private readonly store$: Store) {} + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk { + constructor(store$: Store) { + store$.dispatch(new CustomAction()) + ~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(private readonly store$: Store) { + this.store$.dispatch({ type: 'custom' }) + ~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor(store: Store, private readonly store$: Store) { + store.dispatch(new Login({ payload })); + ~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.store$.dispatch(new AuthActions.dispatch({ type: 'SUCCESS' })); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + nonStore.dispatch(new AuthActions.dispatch({ type: 'FAIL' })); + } + + ngOnInit() { + const store = { dispatch: () => void 0 } + store.dispatch() + } +}`), + fromFixture(` +import { Store } from '@ngrx/store' + +class NotOk3 { + constructor(private readonly store$: Store) {} - ngOnInit() { - this.store$.dispatch(useObject ? { type: 'custom' } : new CustomAction()) - ~~~~~~~~~~~~~~~~~~ [${messageId}] - ~~~~~~~~~~~~~~~~~~ [${messageId}] - } - }`, - ), + ngOnInit() { + this.store$.dispatch(useObject ? { type: 'custom' } : new CustomAction()) + ~~~~~~~~~~~~~~~~~~ [${messageId}] + ~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), ], }) diff --git a/tests/rules/prefer-concat-latest-from.test.ts b/tests/rules/prefer-concat-latest-from.test.ts index 132e5b95..671d34f0 100644 --- a/tests/rules/prefer-concat-latest-from.test.ts +++ b/tests/rules/prefer-concat-latest-from.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { @@ -15,9 +14,9 @@ ruleTester({ ngrxModule: NGRX_MODULE_PATHS.effects, version: '12.1.0' }).run( { code: ` import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - effectOK = createEffect( + +class Ok { + effect = createEffect( () => this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), @@ -33,12 +32,12 @@ class Test { ` import { Actions } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { -readonly effectOK1: CreateEffectMetadata + +class Ok1 { +readonly effect: CreateEffectMetadata constructor(actions$: Actions) { - this.effectOK1 = createEffect(() => ({ scheduler = asyncScheduler } = {}) => { + this.effect = createEffect(() => ({ scheduler = asyncScheduler } = {}) => { return actions$.pipe( ofType(ProductDetailPage.loaded), concatMap((action) => @@ -52,35 +51,35 @@ constructor(actions$: Actions) { ` import { Actions } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -export class Test { -effectOK2 = createEffect(() => - condition - ? this.actions$.pipe( - ofType(ProductDetailPage.loaded), - concatMap((action) => - of(action).pipe( - withLatestFrom(this.store.select$(something), (one, other) => - somethingElse(), + +class Ok2 { + effect = createEffect(() => + condition + ? this.actions$.pipe( + ofType(ProductDetailPage.loaded), + concatMap((action) => + of(action).pipe( + withLatestFrom(this.store.select$(something), (one, other) => + somethingElse(), + ), ), ), - ), - mergeMap(([action, products]) => of(products)), - ) - : this.actions$.pipe(), -) + mergeMap(([action, products]) => of(products)), + ) + : this.actions$.pipe(), + ) -constructor(private readonly actions$: Actions) {} + constructor(private readonly actions$: Actions) {} }`, ], invalid: [ fromFixture( - stripIndent` + ` import { Actions } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - effectNOK = createEffect(() => + +class NotOk { + effect = createEffect(() => this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), withLatestFrom((action) => @@ -96,12 +95,12 @@ class Test { constructor(private readonly actions$: Actions) {} }`, { - output: stripIndent` + output: ` import { Actions, concatLatestFrom } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - effectNOK = createEffect(() => + +class NotOk { + effect = createEffect(() => this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), concatLatestFrom((action) => @@ -118,15 +117,15 @@ class Test { }, ), fromFixture( - stripIndent` + ` import { Actions } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - readonly effectNOK1: CreateEffectMetadata + +class NotOk1 { + readonly effect: CreateEffectMetadata constructor(actions$: Actions) { - this.effectNOK1 = createEffect( + this.effect = createEffect( () => ({ debounce = 300 } = {}) => condition @@ -143,15 +142,15 @@ class Test { } }`, { - output: stripIndent` + output: ` import { Actions, concatLatestFrom } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - readonly effectNOK1: CreateEffectMetadata + +class NotOk1 { + readonly effect: CreateEffectMetadata constructor(actions$: Actions) { - this.effectNOK1 = createEffect( + this.effect = createEffect( () => ({ debounce = 300 } = {}) => condition @@ -169,11 +168,11 @@ class Test { }, ), fromFixture( - stripIndent` + ` import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - effectNOK2 = createEffect(() => ({ debounce = 700 } = {}) => { + +class NotOk2 { + effect = createEffect(() => ({ debounce = 700 } = {}) => { return this.actions$.pipe( ofType(ProductDetailPage.loaded), concatMap((action) => @@ -186,12 +185,11 @@ class Test { }`, { options: [{ strict: true }], - output: stripIndent` -import { concatLatestFrom } from '@ngrx/effects'; + output: `import { concatLatestFrom } from '@ngrx/effects';\n import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - effectNOK2 = createEffect(() => ({ debounce = 700 } = {}) => { + +class NotOk2 { + effect = createEffect(() => ({ debounce = 700 } = {}) => { return this.actions$.pipe( ofType(ProductDetailPage.loaded), concatMap((action) => @@ -204,12 +202,12 @@ class Test { }, ), fromFixture( - stripIndent` + ` import { concatLatestFrom } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - effectNOK3 = createEffect(() => { + +class NotOk3 { + effect = createEffect(() => { return condition ? this.actions$.pipe( ofType(ProductDetailPage.loaded), @@ -226,13 +224,12 @@ class Test { }`, { options: [{ strict: true }], - output: stripIndent` -import { map } from 'rxjs/operators'; + output: `import { map } from 'rxjs/operators';\n import { concatLatestFrom } from '@ngrx/effects' import { of, withLatestFrom } from 'rxjs' -@Injectable() -class Test { - effectNOK3 = createEffect(() => { + +class NotOk3 { + effect = createEffect(() => { return condition ? this.actions$.pipe( ofType(ProductDetailPage.loaded), @@ -259,9 +256,9 @@ ruleTester({ ngrxModule: NGRX_MODULE_PATHS.effects, version: '^11.0.0' }).run( valid: [ ` import { of, withLatestFrom } from 'rxjs'; -@Injectable() -class Test { - effectOK = createEffect( + +class Ok { + effect = createEffect( () => this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), diff --git a/tests/rules/select-style.test.ts b/tests/rules/select-style.test.ts index cb0d79d0..2fbcc7e0 100644 --- a/tests/rules/select-style.test.ts +++ b/tests/rules/select-style.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { @@ -11,243 +10,253 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly test$ = somethingOutside(); - }`, +import { Store } from '@ngrx/store' + +class Ok { + readonly test$ = somethingOutside(); +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok1 { + constructor(private store: Store) {} +}`, ` - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private store: Store) {} - } - `, +import { Store, select } from '@ngrx/store' + +class Ok2 { + constructor(private store: Store) {} +}`, ` - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = select(selector) - constructor(private store: Store) {} - } - `, +import { Store, select } from '@ngrx/store' + +class Ok3 { + foo$ = select(selector) + + constructor(private store: Store) {} +}`, ` - import { select } from '@my-org/framework' - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.pipe(select(selector)) - constructor(private store: Store) {} - } - `, +import { select } from '@my-org/framework' +import { Store } from '@ngrx/store' + +class Ok4 { + foo$ = this.store.pipe(select(selector)) + + constructor(private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select(selector) - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok5 { + foo$ = this.store.select(selector) + + constructor(private store: Store) {} +}`, ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select(selector) - constructor(private store: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok6 { + foo$ = this.store.select(selector) + + constructor(private store: Store) {} +}`, ` - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.customName.select(selector) - constructor(private customName: Store) {} - } - `, +import { Store, select } from '@ngrx/store' + +class Ok7 { + foo$ = this.customName.select(selector) + constructor(private customName: Store) {} +}`, { code: ` - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.pipe(select(selector)) - constructor(private store: Store) {} - } - `, +import { Store, select } from '@ngrx/store' + +class Ok8 { + foo$ = this.store.pipe(select(selector)) + + constructor(private store: Store) {} +}`, options: [SelectStyle.Operator], }, { code: ` - import { select, Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select(selector) - constructor(private store: Store) {} - } - `, +import { select, Store } from '@ngrx/store' + +class Ok9 { + foo$ = this.store.select(selector) + + constructor(private store: Store) {} +}`, options: [SelectStyle.Method], }, ], invalid: [ fromFixture( - stripIndent` - import { select, Store } from '@ngrx/store' - ~~~~~~ [${methodSelectMessageId}] - @Component() - export class FixtureComponent { - foo$ = this.store.pipe( select(selector), ) - ~~~~~~ [${methodSelectMessageId}] - constructor(private store: Store) {} - } - `, + ` +import { select, Store } from '@ngrx/store' + ~~~~~~ [${methodSelectMessageId}] + +class NotOk { + foo$ = this.store.pipe( select(selector), ) + ~~~~~~ [${methodSelectMessageId}] + + constructor(private store: Store) {} +}`, { - output: stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store. select((selector), ) - constructor(private store: Store) {} - } - `, + output: ` +import { Store } from '@ngrx/store' + +class NotOk { + foo$ = this.store. select((selector), ) + + constructor(private store: Store) {} +}`, }, ), fromFixture( - stripIndent` - import { Store, select } from '@ngrx/store' - ~~~~~~ [${methodSelectMessageId}] - @Component() - export class FixtureComponent { - foo$ = this.store.pipe (select - ~~~~~~ [${methodSelectMessageId}] - (selector, selector2), filter(Boolean)) - - constructor(private store: Store) {} - } - `, + ` +import { Store, select } from '@ngrx/store' + ~~~~~~ [${methodSelectMessageId}] + +class NotOk1 { + foo$ = this.store.pipe (select + ~~~~~~ [${methodSelectMessageId}] + (selector, selector2), filter(Boolean)) + + constructor(private store: Store) {} +}`, { options: [SelectStyle.Method], - output: stripIndent` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select - (selector, selector2).pipe ( filter(Boolean)) - - constructor(private store: Store) {} - } - `, + output: ` +import { Store } from '@ngrx/store' + +class NotOk1 { + foo$ = this.store.select + (selector, selector2).pipe ( filter(Boolean)) + + constructor(private store: Store) {} +}`, }, ), fromFixture( - stripIndent` - import { select, Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ - constructor(private store: Store) { - this.foo$ = store.select( - ~~~~~~ [${operatorSelectMessageId}] - selector, - ) - } - } - `, + ` +import { select, Store } from '@ngrx/store' + +class NotOk2 { + bar$: Observable + foo$: Observable + + constructor(store: Store, private readonly customStore: Store) { + this.foo$ = store.select( + ~~~~~~ [${operatorSelectMessageId}] + selector, + ) + } + + ngOnInit() { + this.bar$ = this.customStore.select( + ~~~~~~ [${operatorSelectMessageId}] + selector, + ) + } +}`, { options: [SelectStyle.Operator] as const, - output: stripIndent` - import { select, Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ - constructor(private store: Store) { - this.foo$ = store.pipe(select( - selector, - )) - } - } - `, + output: ` +import { select, Store } from '@ngrx/store' + +class NotOk2 { + bar$: Observable + foo$: Observable + + constructor(store: Store, private readonly customStore: Store) { + this.foo$ = store.pipe(select( + selector, + )) + } + + ngOnInit() { + this.bar$ = this.customStore.pipe(select( + selector, + )) + } +}`, }, ), fromFixture( - stripIndent` - import { - Store, - select, - ~~~~~~ [${methodSelectMessageId}] - } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.pipe(select(selector), map(toItem)).pipe() - ~~~~~~ [${methodSelectMessageId}] - bar$ = this.store. - select(selector).pipe() - baz$ = this.store.pipe( - select(({customers}) => customers), map(toItem), - ~~~~~~ [${methodSelectMessageId}] - ).pipe() - constructor(private store: Store) {} - } - - @Component() - export class FixtureComponent2 { - foo$ = this.store.pipe(select(selector), map(toItem)).pipe() - ~~~~~~ [${methodSelectMessageId}] - constructor(private readonly store: Store) {} - } - `, + ` +import { + Store, + select, + ~~~~~~ [${methodSelectMessageId}] +} from '@ngrx/store' + +class NotOk3 { + foo$ = this.store.pipe(select(selector), map(toItem)).pipe() + ~~~~~~ [${methodSelectMessageId}] + bar$ = this.store. + select(selector).pipe() + baz$ = this.store.pipe( + select(({ customers }) => customers), map(toItem), + ~~~~~~ [${methodSelectMessageId}] + ).pipe() + + constructor(private store: Store) {} +} + +class NotOk4 { + foo$ = this.store.pipe(select(selector), map(toItem)).pipe() + ~~~~~~ [${methodSelectMessageId}] + + constructor(private readonly store: Store) {} +}`, { options: [SelectStyle.Method], - output: stripIndent` - import { - Store, - } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select(selector).pipe( map(toItem)).pipe() - bar$ = this.store. - select(selector).pipe() - baz$ = this.store.select(({customers}) => customers).pipe( - map(toItem), - ).pipe() - constructor(private store: Store) {} - } - - @Component() - export class FixtureComponent2 { - foo$ = this.store.select(selector).pipe( map(toItem)).pipe() - constructor(private readonly store: Store) {} - } - `, + output: ` +import { + Store, +} from '@ngrx/store' + +class NotOk3 { + foo$ = this.store.select(selector).pipe( map(toItem)).pipe() + bar$ = this.store. + select(selector).pipe() + baz$ = this.store.select(({ customers }) => customers).pipe( + map(toItem), + ).pipe() + + constructor(private store: Store) {} +} + +class NotOk4 { + foo$ = this.store.select(selector).pipe( map(toItem)).pipe() + + constructor(private readonly store: Store) {} +}`, }, ), fromFixture( - stripIndent` - import type {Creator} from '@ngrx/store' - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.select(selector) - ~~~~~~ [${operatorSelectMessageId}] - constructor(private store: Store) {} - } - `, + ` +import type {Creator} from '@ngrx/store' +import { Store } from '@ngrx/store' + +class NotOk5 { + foo$ = this.store.select(selector) + ~~~~~~ [${operatorSelectMessageId}] + + constructor(private store: Store) {} +}`, { options: [SelectStyle.Operator], - output: stripIndent` - import type {Creator} from '@ngrx/store' - import { Store, select } from '@ngrx/store' - @Component() - export class FixtureComponent { - foo$ = this.store.pipe(select(selector)) - constructor(private store: Store) {} - } - `, + output: ` +import type {Creator} from '@ngrx/store' +import { Store, select } from '@ngrx/store' + +class NotOk5 { + foo$ = this.store.pipe(select(selector)) + + constructor(private store: Store) {} +}`, }, ), ], diff --git a/tests/rules/updater-explicit-return-type.test.ts b/tests/rules/updater-explicit-return-type.test.ts index 0649a369..c9e37da3 100644 --- a/tests/rules/updater-explicit-return-type.test.ts +++ b/tests/rules/updater-explicit-return-type.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { @@ -6,116 +5,115 @@ import rule, { } from '../../src/rules/component-store/updater-explicit-return-type' import { ruleTester } from '../utils' -const setup = ` - import type { SelectConfig } from '@ngrx/component-store' - import { Projector } from '@ngrx/component-store' - import { ComponentStore } from '@ngrx/component-store' - - interface Movie { - readonly genre: string; - readonly title: string; - } - - interface MoviesState { - readonly movies: readonly Movie[]; - } -` - ruleTester().run(path.parse(__filename).name, rule, { valid: [ - `${setup} + ` +import { ComponentStore } from '@ngrx/component-store' - export class MoviesStore extends ComponentStore { - constructor() { - super({movies: []}); - } +class Ok extends ComponentStore { + readonly addMovie = this.updater( + (state, movie): MoviesState => ({ movies: [...state.movies, movie] }), + ) - readonly addMovie = this.updater((state, movie): MoviesState => ({ movies: [...state.movies, movie] })); - }`, - `${setup} - - export class MoviesStore extends ComponentStore { - constructor() { - super({movies: []}); - } - - readonly addMovie = this.updater((state, movie): MoviesState => ({ movies: [...state.movies, movie] })); - }`, - `${setup} + constructor() { + super({ movies: [] }) + } +}`, + ` +import { ComponentStore } from '@ngrx/component-store' - export class MoviesStore { - constructor(private readonly store: ComponentStore) {} +class Ok1 extends ComponentStore { + readonly addMovie = this.updater( + (state, movie): MoviesState => ({ movies: [...state.movies, movie] }), + ) - readonly addMovie = this.store.updater((state, movie): MoviesState => ({ + constructor() { + super({ movies: [] }) + } +}`, + ` +import { ComponentStore } from '@ngrx/component-store' + +class Ok2 { + readonly addMovie = this.store.updater( + (state, movie): MoviesState => ({ + movies: [...state.movies, movie], + }), + ) + + constructor(private readonly store: ComponentStore) {} +}`, + ` +import { ComponentStore } from '@ngrx/component-store' + +class Ok3 { + readonly addMovie: Observable + + constructor(customStore: ComponentStore) { + this.addMovie = customStore.updater( + (state, movie): MoviesState => ({ movies: [...state.movies, movie], - })); - }`, - `${setup} - - export class MoviesStore { - readonly addMovie: CreateEffectMetadata - - constructor(customStore: ComponentStore) { - this.addMovie = customStore.updater((state, movie): MoviesState => ({ - movies: [...state.movies, movie], - })); - } - }`, + }), + ) + } +}`, ], invalid: [ - fromFixture( - stripIndent` - ${setup} - - export class MoviesStore extends ComponentStore { - constructor() { - super({movies: []}); - } - - readonly addMovie = this.updater((state, movie) => ({ movies: [...state.movies, movie] })); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - `, - ), - fromFixture( - stripIndent` - ${setup} - - export class MoviesStore extends ComponentStore { - constructor() { - super({movies: []}); - } - - readonly addMovie = this.updater((state, movie) => movie ? ({ movies: [...state.movies, movie] }) : ({ movies })); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - `, - ), - fromFixture( - stripIndent` - ${setup} - - export class MoviesStore { - constructor(private readonly store: ComponentStore) {} - - readonly addMovie = this.store.updater((state, movie) => ({ movies: [...state.movies, movie] })); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - `, - ), - fromFixture( - stripIndent` - ${setup} - - export class MoviesStore { - readonly addMovie: CreateEffectMetadata - - constructor(customStore: ComponentStore) { - this.addMovie = customStore.updater((state, movie) => ({ movies: [...state.movies, movie] })); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] - } - } - `, - ), + fromFixture(` +import { ComponentStore } from '@ngrx/component-store' + +class NotOk extends ComponentStore { + readonly addMovie = this.updater((state, movie) => ({ movies: [...state.movies, movie] })) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + + constructor() { + super({ movies: [] }) + } +}`), + fromFixture(` +import { ComponentStore } from '@ngrx/component-store' + +class NotOk1 extends ComponentStore { + readonly updateMovie: Observable + readonly addMovie = this.updater((state, movie) => movie ? ({ movies: [...state.movies, movie] }) : ({ movies })) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + + constructor(componentStore: ComponentStore) { + super({ movies: [] }) + this.updateMovie = componentStore.updater(() => ({ movies: MOVIES })) + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + } +}`), + fromFixture(` +import { ComponentStore } from '@ngrx/component-store' + +class NotOk2 { + readonly addMovie = this.store.updater((state, movie) => ({ movies: [...state.movies, movie] })) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + + constructor(private readonly store: ComponentStore) {} +}`), + fromFixture(` +import { ComponentStore } from '@ngrx/component-store' + +class NotOk3 { + readonly addMovie: Observable + readonly updateMovie: Observable + + constructor( + customStore: ComponentStore, + private readonly store: ComponentStore + ) { + this.addMovie = customStore.updater((state, movie) => ({ movies: [...state.movies, movie] })) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + this.updateMovie = this.store.updater(() => ({ movies: MOVIES })) + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [${messageId}] + } + + ngOnInit() { + const updater = (item: Movie) => item + updater() + } +}`), ], }) diff --git a/tests/rules/use-consistent-global-store-name.test.ts b/tests/rules/use-consistent-global-store-name.test.ts index 3d968afd..4671b770 100644 --- a/tests/rules/use-consistent-global-store-name.test.ts +++ b/tests/rules/use-consistent-global-store-name.test.ts @@ -1,4 +1,4 @@ -import { stripIndents } from 'common-tags' +import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { useConsistentGlobalStoreName, @@ -8,95 +8,138 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ + ` +class Ok {}`, + ` +import { Store } from '@ngrx/store' + +class Ok1 { + constructor(store: Store) {} +}`, { code: ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private store: Store) {} - } - `, - }, - { - code: ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private customName: Store) {} - } - `, +import { Store } from '@ngrx/store' + +class Ok2 { + constructor(private customName: Store) {} +}`, options: ['customName'], }, ], invalid: [ - { - code: stripIndents` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private readonly somethingElse$: Store) {} - } - `, - errors: [ - { - column: 30, - endColumn: 44, - line: 4, - messageId: useConsistentGlobalStoreName, - data: { - storeName: 'store', + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk { + constructor(private readonly somethingElse$: Store) {} + ~~~~~~~~~~~~~~ [${useConsistentGlobalStoreName} { "storeName": "store" }] +}`, + { + suggestions: [ + { + messageId: useConsistentGlobalStoreNameSuggest, + data: { + storeName: 'store', + }, + output: ` +import { Store } from '@ngrx/store' + +class NotOk { + constructor(private readonly store: Store) {} +}`, }, - suggestions: [ - { - messageId: useConsistentGlobalStoreNameSuggest, - data: { - storeName: 'store', - }, - output: stripIndents` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private readonly store: Store) {} - }`, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(private readonly store1: Store, private readonly store: Store) {} + ~~~~~~ [${useConsistentGlobalStoreName} { "storeName": "store" }] +}`, + { + suggestions: [ + { + messageId: useConsistentGlobalStoreNameSuggest, + data: { + storeName: 'store', }, - ], - }, - ], - }, - { - code: stripIndents` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private store: Store) {} - } - `, - options: ['customName'], - errors: [ - { - column: 21, - endColumn: 26, - line: 4, - messageId: useConsistentGlobalStoreName, - data: { - storeName: 'customName', + output: ` +import { Store } from '@ngrx/store' + +class NotOk1 { + constructor(private readonly store: Store, private readonly store: Store) {} +}`, }, - suggestions: [ - { - messageId: useConsistentGlobalStoreNameSuggest, - data: { - storeName: 'customName', - }, - output: stripIndents` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - constructor(private customName: Store) {} - }`, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor(private readonly store1: Store, private readonly store2: Store) {} + ~~~~~~ [${useConsistentGlobalStoreName} { "storeName": "store" } suggest 0] + ~~~~~~ [${useConsistentGlobalStoreName} { "storeName": "store" } suggest 1] +}`, + { + suggestions: [ + { + messageId: useConsistentGlobalStoreNameSuggest, + data: { + storeName: 'store', }, - ], - }, - ], - }, + output: ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor(private readonly store: Store, private readonly store2: Store) {} +}`, + }, + { + messageId: useConsistentGlobalStoreNameSuggest, + data: { + storeName: 'store', + }, + output: ` +import { Store } from '@ngrx/store' + +class NotOk2 { + constructor(private readonly store1: Store, private readonly store: Store) {} +}`, + }, + ], + }, + ), + fromFixture( + ` +import { Store } from '@ngrx/store' + +class NotOk3 { + constructor(private store: Store) {} + ~~~~~ [${useConsistentGlobalStoreName} { "storeName": "customName" }] +}`, + { + options: ['customName'], + suggestions: [ + { + messageId: useConsistentGlobalStoreNameSuggest, + data: { + storeName: 'customName', + }, + output: ` +import { Store } from '@ngrx/store' + +class NotOk3 { + constructor(private customName: Store) {} +}`, + }, + ], + }, + ), ], }) diff --git a/tests/rules/use-selector-in-select.test.ts b/tests/rules/use-selector-in-select.test.ts index fe296005..bfbd1192 100644 --- a/tests/rules/use-selector-in-select.test.ts +++ b/tests/rules/use-selector-in-select.test.ts @@ -1,4 +1,3 @@ -import { stripIndent } from 'common-tags' import { fromFixture } from 'eslint-etc' import path from 'path' import rule, { messageId } from '../../src/rules/store/use-selector-in-select' @@ -7,126 +6,156 @@ import { ruleTester } from '../utils' ruleTester().run(path.parse(__filename).name, rule, { valid: [ ` - import { Store } from '@ngrx/store' - @Component() - export class FixtureComponent { - readonly test$ = somethingOutside(); - }`, - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.pipe(select(selectCustomers)) - constructor(store: Store){} - }`, - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.select(selectCustomers) - constructor(store: Store){} - }`, - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.pipe(select(selectorsObj.selectCustomers)) - constructor(store: Store){} - }`, - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.select(selectorsObj.selectCustomers) - constructor(store: Store){} - }`, +class Ok { + readonly test$ = somethingOutside(); +}`, + ` +import { Store } from '@ngrx/store' + +class Ok1 { + view$: Observable + + constructor(store: Store) { + this.view$ = store.pipe(select(selectCustomers)) + } +}`, + ` +import { Store } from '@ngrx/store' + +class Ok2 { + view$: Observable + + constructor(private store: Store) { + this.view$ = store.select(selectCustomers) + } +}`, + ` +import { Store } from '@ngrx/store' + +class Ok3 { + view$ = this.store.pipe(select(CustomerSelectors.selectCustomers)) + + constructor(private store: Store) {} +}`, + ` +import { Store } from '@ngrx/store' + +class Ok4 { + view$ = this.store.select(selectCustomers) + + constructor(private readonly store: Store) {} +}`, // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/41 - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.pipe(select(selectQueryParam('parameter'))) - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class Ok5 { + view$ = this.store.pipe(select(selectQueryParam('parameter'))) + + constructor(private store: Store) {} +}`, // https://github.com/timdeschryver/eslint-plugin-ngrx/issues/135 - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.select(this.store.select(hasAuthorization, 'ADMIN')); - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class Ok6 { + view$ = this.store.select(this.store.select(hasAuthorization, 'ADMIN')); + + constructor(private readonly store: Store) {} +}`, ], invalid: [ fromFixture( - stripIndent` - import type { Selector } from '@ngrx/store' - import { MemoizeFn } from '@ngrx/store' - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.pipe(select('customers')) - ~~~~~~~~~~~ [${messageId}] - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk { + view$: Observable + + constructor(store: Store) { + this.view$ = this.store.pipe(select('customers')) + ~~~~~~~~~~~ [${messageId}] + } +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.select('customers') - ~~~~~~~~~~~ [${messageId}] - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk1 { + view$ = this.store.select('customers') + ~~~~~~~~~~~ [${messageId}] + + constructor(private store: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.pipe(select('customers', 'orders')) - ~~~~~~~~~~~ [${messageId}] - ~~~~~~~~ [${messageId}] - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk2 { + view$ = this.store.pipe(select('customers', 'orders')) + ~~~~~~~~~~~ [${messageId}] + ~~~~~~~~ [${messageId}] + + constructor(private readonly store: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.select('customers', 'orders') - ~~~~~~~~~~~ [${messageId}] - ~~~~~~~~ [${messageId}] - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk3 { + view$ = this.store.select('customers', 'orders') + ~~~~~~~~~~~ [${messageId}] + ~~~~~~~~ [${messageId}] + + constructor(private store: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.pipe(select(s => s.customers)) - ~~~~~~~~~~~~~~~~ [${messageId}] - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk4 { + view$ = this.store.pipe(select(s => s.customers)) + ~~~~~~~~~~~~~~~~ [${messageId}] + + constructor(private store: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store.select(s => s.customers) - ~~~~~~~~~~~~~~~~ [${messageId}] - constructor(store: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk5 { + view$ = this.store.select(s => s.customers) + ~~~~~~~~~~~~~~~~ [${messageId}] + + constructor(readonly store: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store$.select(s => s.customers) - ~~~~~~~~~~~~~~~~ [${messageId}] - constructor(store$: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk6 { + view$ = this.store$.select(s => s.customers) + ~~~~~~~~~~~~~~~~ [${messageId}] + + constructor(private store$: Store) {} +}`, ), fromFixture( - stripIndent` - import { Store } from '@ngrx/store' - export class Component { - view$ = this.store$.pipe(select('customers')) - ~~~~~~~~~~~ [${messageId}] - constructor(store$: Store){} - }`, + ` +import { Store } from '@ngrx/store' + +class NotOk7 { + view$ = this.store$.pipe(select('customers')) + ~~~~~~~~~~~ [${messageId}] + + constructor(private readonly store$: Store) {} +}`, ), ], })