Skip to content
This repository has been archived by the owner on Jul 11, 2024. It is now read-only.

Commit

Permalink
fix: report rules on multiple instances (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelss95 authored Oct 12, 2021
1 parent 7cfd30b commit c392618
Show file tree
Hide file tree
Showing 32 changed files with 2,047 additions and 1,764 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
}
4 changes: 3 additions & 1 deletion scripts/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const placeholder = '<!-- MANUAL-DOC:START -->'
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 = [
Expand Down
14 changes: 8 additions & 6 deletions src/rules/component-store/updater-explicit-return-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -29,13 +30,14 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
},
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)
Expand Down
13 changes: 9 additions & 4 deletions src/rules/effects/avoid-cyclic-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,8 +39,12 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
},
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)

Expand Down Expand Up @@ -145,7 +150,7 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
}

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,
}
},
Expand Down
13 changes: 9 additions & 4 deletions src/rules/effects/no-dispatch-in-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -39,11 +40,15 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
},
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)
Expand Down
31 changes: 19 additions & 12 deletions src/rules/effects/prefer-concat-latest-from.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -52,8 +53,7 @@ export default createRule<Options, MessageIds>({
},
defaultOptions: [defaultOptions],
create: (context, [options]) => {
const sourceCode = context.getSourceCode()
const selector = getSelector(context, options)
const { selector, sourceCode } = getSelectorWithSourceCode(context, options)

return {
...(selector && {
Expand All @@ -69,23 +69,30 @@ export default createRule<Options, MessageIds>({
},
})

function getSelector(
function getSelectorWithSourceCode(
context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
{ 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(
Expand Down
23 changes: 14 additions & 9 deletions src/rules/store/avoid-combining-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -31,13 +32,17 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
},
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 {
Expand Down
21 changes: 15 additions & 6 deletions src/rules/store/avoid-dispatching-multiple-actions-sequentially.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,15 +30,19 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
},
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<TSESTree.CallExpression>()

return {
[`BlockStatement > ExpressionStatement > ${storeDispatch(storeName)}`](
node: TSESTree.CallExpression,
) {
[`BlockStatement > ExpressionStatement > ${dispatchExpression(
storeNames,
)}`](node: TSESTree.CallExpression) {
collectedDispatches.add(node)
},
'BlockStatement:exit'() {
Expand Down
23 changes: 14 additions & 9 deletions src/rules/store/avoid-mapping-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -31,14 +32,18 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
},
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 {
Expand Down
68 changes: 43 additions & 25 deletions src/rules/store/no-multiple-global-stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -35,38 +34,39 @@ export default ESLintUtils.RuleCreator(docsUrl)<Options, MessageIds>({
},
defaultOptions: [],
create: (context) => {
const sourceCode = context.getSourceCode()
const collectedStores = new Set<TSESTree.Identifier>()

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<TSESLint.SourceCode>,
fixer: TSESLint.RuleFixer,
Expand All @@ -76,3 +76,21 @@ function getFixes(
const nodeToRemove = parent && isTSParameterProperty(parent) ? parent : node
return getNodeToCommaRemoveFix(sourceCode, fixer, nodeToRemove)
}

type Identifiers = NonNullable<ReturnType<typeof getNgRxStores>['identifiers']>

function groupBy(identifiers: Identifiers): Map<TSESTree.Node, Identifiers> {
return identifiers.reduce<Map<TSESTree.Node, Identifiers>>(
(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(),
)
}
Loading

0 comments on commit c392618

Please sign in to comment.