Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(packages/sui-decorators): Create AsyncInlineError decorator #1746

Merged
merged 19 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bc73cff
feat(packages/sui-decorators): Create AsyncInlineError decorator
oriolpuig Mar 27, 2024
2b89c71
feat(packages/eslint-plugin-sui): Decorators ADR: validate AsyncInlin…
oriolpuig May 29, 2024
5ea3a37
feat(packages/eslint-plugin-sui): Decorators ADR: AsyncInlineError mu…
oriolpuig May 29, 2024
2a184b6
feat(packages/eslint-plugin-sui): Decorators ADR: @AsyncInlineError m…
oriolpuig May 29, 2024
9a44253
feat(packages/eslint-plugin-sui): Decorators ADR: check decorators or…
oriolpuig May 29, 2024
f3a2001
feat(packages/eslint-plugin-sui): [ADR] Decorators: Use @AsyncInlineE…
oriolpuig Jun 6, 2024
d830f2b
feat(packages/eslint-plugin-sui): ADR Decorators: use MethodDefinitio…
oriolpuig Jun 12, 2024
4bab675
feat(packages/eslint-plugin-sui): Rename rule to decorator-async-inli…
oriolpuig Jul 2, 2024
6294dd8
feat(packages/sui-lint): Add sui/decorator-async-inline-error rule to…
oriolpuig Jul 2, 2024
fe8b7f5
chore(Root): Update package-lock
oriolpuig Jul 2, 2024
bee8cb2
docs(packages/sui-decorators): Add documentation for new @AsyncInline…
oriolpuig Jul 2, 2024
d997ff7
feat(packages/sui-decorators): @inlineError - Add warning to avoid us…
oriolpuig Jul 2, 2024
6124f6b
feat(packages/eslint-plugin-sui): Create new rule for decorator-inlin…
oriolpuig Jul 2, 2024
3d7d977
feat(packages/sui-lint): Add new decorator-inline-error to domain pac…
oriolpuig Jul 2, 2024
163a959
feat(packages/eslint-plugin-sui): @inlineError must be on non-async f…
oriolpuig Jul 3, 2024
ef3c679
refactor(packages/eslint-plugin-sui): Clean @AsyncInlineError decorat…
oriolpuig Jul 3, 2024
dc694df
Merge branch 'master' into poc/create_new_async_inline_error
oriolpuig Jul 31, 2024
baeeb54
docs(packages/sui-decorators): Update readme with deprecated decorato…
oriolpuig Jul 31, 2024
df766db
refactor(packages/eslint-plugin-sui): Create helper methods to improv…
oriolpuig Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/eslint-plugin-sui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ const FactoryPattern = require('./rules/factory-pattern.js')
const SerializeDeserialize = require('./rules/serialize-deserialize.js')
const CommonJS = require('./rules/commonjs.js')
const Decorators = require('./rules/decorators.js')
const DecoratorAsyncInlineError = require('./rules/decorator-async-inline-error.js')
const DecoratorDeprecated = require('./rules/decorator-deprecated.js')
const DecoratorDeprecatedRemarkMethod = require('./rules/decorator-deprecated-remark-method.js')
const DecoratorInlineError = require('./rules/decorator-inline-error.js')
const LayersArch = require('./rules/layers-architecture.js')

// ------------------------------------------------------------------------------
Expand All @@ -17,8 +19,10 @@ module.exports = {
'serialize-deserialize': SerializeDeserialize,
commonjs: CommonJS,
decorators: Decorators,
'layers-arch': LayersArch,
'decorator-async-inline-error': DecoratorAsyncInlineError,
'decorator-deprecated': DecoratorDeprecated,
'decorator-deprecated-remark-method': DecoratorDeprecatedRemarkMethod
'decorator-deprecated-remark-method': DecoratorDeprecatedRemarkMethod,
'decorator-inline-error': DecoratorInlineError,
'layers-arch': LayersArch
}
}
113 changes: 113 additions & 0 deletions packages/eslint-plugin-sui/src/rules/decorator-async-inline-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @fileoverview Ensure that at least all your UseCases, Services and Repositories are using @AsyncInlineError decorator from sui
*/
'use strict'

const dedent = require('string-dedent')
const {getDecoratorsByNode} = require('../utils/decorators.js')
const {isAUseCase, isAService, isARepository} = require('../utils/domain.js')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Ensure that at least all your UseCases, Services and Repositories are using @AsyncInlineError decorator from sui',
recommended: true,
url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements'
},
fixable: 'code',
schema: [],
messages: {
notFoundAsyncInlineErrorDecoratorOnUseCase: dedent`
The execute method of a UseCase should use the @AsyncInlineError() decorator in order to follow the Adevinta domain code guidelines.
`,
notFoundAsyncInlineErrorDecoratorOnService: dedent`
The execute method of a Service should use the @AsyncInlineError() decorator in order to follow the Adevinta domain code guidelines.
`,
notFoundAsyncInlineErrorDecoratorOnRepository: dedent`
The public Repository methods should use the @AsyncInlineError() decorator in order to follow the Adevinta domain code guidelines.
`,
asyncInlineErrorDecoratorIsNotFirst: dedent`
The @AsyncInlineError() decorator must always be closest to the execute method to avoid inconsistence with other decorators.
`
}
},
create: function (context) {
return {
MethodDefinition(node) {
// Method
const method = node
const methodName = method.key?.name
const isExecuteMethod = methodName === 'execute'

// Class
const classObject = node.parent?.parent
const isUsecase = isAUseCase({context, classObject})
const isService = isAService({context, classObject})
const isRepository = isARepository({context, classObject})

// Skip if it's not a UseCase, Service or Repository
if (!isUsecase && !isService && !isRepository && !isExecuteMethod) return

// Skip if a constructor or a not public method (starts by _ or #)
if (methodName === 'constructor') return
if (methodName.startsWith('_')) return
if (methodName.startsWith('#')) return
if ((isUsecase || isService) && !isExecuteMethod) return

// Method decorators
const methodDecorators = getDecoratorsByNode(node, {isAMethod: true})
const hasDecorators = methodDecorators?.length > 0

// Get the @AsyncInlineError decorator from method
const asyncInlineErrorDecoratorNode =
hasDecorators &&
methodDecorators?.find(decorator => decorator?.expression?.callee?.name === 'AsyncInlineError')

// Check if the @AsyncInlineError decorator is the last one
const isAsyncInlineErrorLastDecorator =
hasDecorators && methodDecorators?.at(-1)?.expression?.callee?.name === 'AsyncInlineError'

// RULE: The method should have the @AsyncInlineError decorator
if (!asyncInlineErrorDecoratorNode && isUsecase) {
context.report({
node: method.key,
messageId: 'notFoundAsyncInlineErrorDecoratorOnUseCase'
})
}

if (!asyncInlineErrorDecoratorNode && isService) {
context.report({
node: method.key,
messageId: 'notFoundAsyncInlineErrorDecoratorOnService'
})
}

if (!asyncInlineErrorDecoratorNode && isRepository) {
context.report({
node: method.key,
messageId: 'notFoundAsyncInlineErrorDecoratorOnRepository'
})
}

// RULE: The @AsyncInlineError decorator should be the first one, to avoid inconsistencies with other decorators
if (asyncInlineErrorDecoratorNode && !isAsyncInlineErrorLastDecorator) {
context.report({
node: asyncInlineErrorDecoratorNode,
messageId: 'asyncInlineErrorDecoratorIsNotFirst',
*fix(fixer) {
yield fixer.remove(asyncInlineErrorDecoratorNode)
yield fixer.insertTextAfter(methodDecorators.at(-1), '\n@AsyncInlineError()')
}
})
}
}
}
}
}
139 changes: 139 additions & 0 deletions packages/eslint-plugin-sui/src/rules/decorator-inline-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* @fileoverview Ensure the right usage of @inlineError decorator from sui in sui-domain
*/
'use strict'

const dedent = require('string-dedent')
const path = require('path')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ensure the right usage of @inlineError decorator from sui in sui-domain',
recommended: true,
url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements'
},
fixable: 'code',
schema: [],
messages: {
avoidUseInlineErrorOnAsyncFunctions: dedent`
The @inlineError decorator is deprecated on async functions. Use the @AsyncInlineError() decorator instead.
`,
useInlineErrorOnNonAsyncFunctions: dedent`
The @inlineError decorator should be used on non async functions.
`,
inlineErrorDecoratorIsNotFirst: dedent`
The @inlineError decorator must always be closest to the execute method to avoid inconsistence with other decorators.
`
}
},
create: function (context) {
const asyncInlineErrorImportStatement = "import {AsyncInlineError} from '@s-ui/decorators';\n"

const filePath = context.getFilename()
const relativePath = path.relative(context.getCwd(), filePath)

// Check if the file is inside requierd folders (useCases, services, repositories, ...)
const useCasePattern = /useCases|usecases/i
const isUseCasePath = useCasePattern.test(relativePath)

const servicePattern = /services/i
const isServicePath = servicePattern.test(relativePath)

const repositoryPattern = /repositories/i
const isRepositoryPath = repositoryPattern.test(relativePath)

return {
MethodDefinition(node) {
// Method
const method = node
const isAsync = method?.value?.async || false
const methodName = method.key?.name
const isExecuteMethod = methodName === 'execute'

// Class
const classObject = node.parent?.parent
const className = classObject?.id?.name
const superClassName = classObject?.superClass?.name

// UseCase
const containUseCase = className?.endsWith('UseCase')
const extendsUseCase = superClassName === 'UseCase'
const isUsecase = containUseCase || extendsUseCase || isUseCasePath

// Service
const containService = className?.endsWith('Service')
const extendsService = superClassName === 'Service'
const isService = containService || extendsService || isServicePath

// Repository
const containRepository = className?.endsWith('Repository')
const extendsRepository = superClassName === 'Repository'
const isRepository = containRepository || extendsRepository || isRepositoryPath

// Skip if it's not a UseCase, Service or Repository
if (!isUsecase && !isService && !isRepository && !isExecuteMethod) return

// Skip if a constructor or a not public method (starts by _ or #)
if (methodName === 'constructor') return
if (methodName.startsWith('_')) return
if (methodName.startsWith('#')) return
if ((isUsecase || isService) && !isExecuteMethod) return

// Method decorators
const methodDecorators = method.decorators
const hasDecorators = methodDecorators?.length > 0

// Get the @inlineError decorator from method
const inlineErrorDecoratorNode =
hasDecorators && methodDecorators?.find(decorator => decorator?.expression?.name === 'inlineError')

// Check if the @inlineError decorator is the last one
const isInlineErrorLastDecorator = hasDecorators && methodDecorators?.at(-1)?.expression?.name === 'inlineError'

// TODO: Pending to check if a function is returning a promise (not using async/await syntax)
// RULE: An async function MUST use the @AsyncInlineError() decorator
if (inlineErrorDecoratorNode && isAsync) {
context.report({
node: inlineErrorDecoratorNode,
messageId: 'avoidUseInlineErrorOnAsyncFunctions',
*fix(fixer) {
yield fixer.remove(inlineErrorDecoratorNode)
yield fixer.insertTextAfter(methodDecorators.at(-1), '\n@AsyncInlineError()')
yield fixer.insertTextBeforeRange([0, 0], asyncInlineErrorImportStatement)
}
})
}

// @inlineError decorator should be used on non async functions
if (!isAsync) {
// RULE: A non-async function should use the @inlineError decorator should be the first one
if (!inlineErrorDecoratorNode) {
context.report({
node: method.key,
messageId: 'useInlineErrorOnNonAsyncFunctions'
})
}

// RULE: The @inlineError decorator should be the first one, to avoid inconsistencies with other decorators.
if (inlineErrorDecoratorNode && !isInlineErrorLastDecorator) {
context.report({
node: inlineErrorDecoratorNode,
messageId: 'inlineErrorDecoratorIsNotFirst',
*fix(fixer) {
yield fixer.remove(inlineErrorDecoratorNode)
yield fixer.insertTextAfter(methodDecorators.at(-1), '\n@inlineError')
}
})
}
}
}
}
}
}
27 changes: 2 additions & 25 deletions packages/eslint-plugin-sui/src/rules/decorators.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @fileoverview Ensure that at least all your UseCases are using @inlineError and @tracer decorator from sui
* @fileoverview Ensure that at least all your UseCases are using the @tracer decorator from sui
*/
'use strict'

Expand All @@ -14,24 +14,18 @@ module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ensure that at least all your UseCases are using @inlineError and @tracer decorator from sui',
description: 'Ensure that at least all your UseCases are using the @tracer decorator from sui',
recommended: true,
url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements'
},
fixable: 'code',
schema: [],
messages: {
missingInlineError: dedent`
All our UseCases must have an @inlineError decorator.
`,
missingTracer: dedent`
All our UseCases must have a @tracer() decorator.
`,
tracerMissCall: dedent`
Your tracer decorator should be call always with the name of your class
`,
inlineErrorMissplace: dedent`
The inlineError decorator should always be closest to the execute method
`
}
},
Expand All @@ -41,28 +35,11 @@ module.exports = {
const className = node.parent?.parent?.id?.name
const shouldExtendFromUseCase = node.parent?.parent?.superClass?.name === 'UseCase'
const isExecute = node.key?.name === 'execute' && shouldExtendFromUseCase
const hasInlineError = node.decorators?.some(node => node.expression?.name === 'inlineError')
const tracerNode = node.decorators?.find(node => node.expression?.callee?.name === 'tracer')
const isTracerCalledWithClassName =
tracerNode?.expression?.callee?.name === 'tracer' &&
className + '#' + node.key?.name === tracerNode?.expression?.arguments[0]?.properties[0]?.value?.value &&
tracerNode?.expression?.arguments[0]?.properties[0]?.key?.name === 'metric'
const isInlineErrorTheFirst = node.decorators?.at(-1)?.expression?.name === 'inlineError'

isExecute &&
!hasInlineError &&
context.report({
node: node.key,
messageId: 'missingInlineError'
})

isExecute &&
hasInlineError &&
!isInlineErrorTheFirst &&
context.report({
node: node.key,
messageId: 'inlineErrorMissplace'
})

isExecute &&
!tracerNode &&
Expand Down
Loading