Skip to content

Commit

Permalink
Merge pull request #1785 from SUI-Components/poc/create_new_deprecate…
Browse files Browse the repository at this point in the history
…d_decorator

feat(packages/sui-decorators): Create @deprecated() decorator
  • Loading branch information
oriolpuig authored Jul 31, 2024
2 parents 70b6f9f + ec08bf4 commit c8e2bce
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 3 deletions.
6 changes: 5 additions & 1 deletion packages/eslint-plugin-sui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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 DecoratorDeprecated = require('./rules/decorator-deprecated.js')
const DecoratorDeprecatedRemarkMethod = require('./rules/decorator-deprecated-remark-method.js')
const LayersArch = require('./rules/layers-architecture.js')

// ------------------------------------------------------------------------------
Expand All @@ -15,6 +17,8 @@ module.exports = {
'serialize-deserialize': SerializeDeserialize,
commonjs: CommonJS,
decorators: Decorators,
'layers-arch': LayersArch
'layers-arch': LayersArch,
'decorator-deprecated': DecoratorDeprecated,
'decorator-deprecated-remark-method': DecoratorDeprecatedRemarkMethod
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @fileoverview Ensure that method using @Deprecated() displays a warning alert
*/
'use strict'

const dedent = require('string-dedent')
const {getDecoratorsByNode, getElementMessageName, getElementName, remarkElement} = require('../utils/decorators.js')

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

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ensure that method using @Deprecated() displays a warning alert',
recommended: true,
url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements'
},
fixable: 'code',
schema: [],
messages: {
remarkWarningMessage: dedent`
The {{methodName}} is marked as a deprecated.
`
}
},
create: function (context) {
function highlightNode(node) {
const isAClass = node.type === 'ClassDeclaration'
const isArrowFunction = node.type === 'ArrowFunctionExpression'
const isAMethod = node.type === 'MethodDefinition'

const nodeName = getElementName(node, {isAClass, isAMethod, isArrowFunction})
const decorators = getDecoratorsByNode(node, {isAClass, isAMethod, isArrowFunction})
const hasDecorators = decorators?.length > 0

// Get the @Deprecated() decorator from node decorators
const deprecatedDecoratorNode =
hasDecorators && decorators?.find(decorator => decorator?.expression?.callee?.name === 'Deprecated')

if (!deprecatedDecoratorNode) return

const elementMessageName = getElementMessageName(nodeName, {isAClass, isAMethod, isArrowFunction})
const nodeToRemark = remarkElement(node, {isAClass, isAMethod, isArrowFunction})

// RULE: Mark method with a warning
context.report({
node: nodeToRemark,
messageId: 'remarkWarningMessage',
data: {
methodName: elementMessageName
}
})
}

return {
ClassDeclaration: highlightNode,
ArrowFunctionExpression: highlightNode,
MethodDefinition: highlightNode
}
}
}
119 changes: 119 additions & 0 deletions packages/eslint-plugin-sui/src/rules/decorator-deprecated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @fileoverview Ensure that @Deprecated() decorator is used as expected
*/
'use strict'

const dedent = require('string-dedent')
const {getDecoratorsByNode, getElementName, getElementMessageName} = require('../utils/decorators')

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

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ensure that @Deprecated() decorator is used as expected',
recommended: true,
url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements'
},
fixable: 'code',
schema: [],
messages: {
notFoundDecoratorArgumentError: dedent`
The @Deprecated() decorator must have arguments.
`,
notFoundKeyDecoratorArgumentError: dedent`
The @Deprecated() decorator must have a key property.
`,
notFoundMessageDecoratorArgumentError: dedent`
The @Deprecated() decorator must have a message property.
`
}
},
create: function (context) {
function ruleRunner(node) {
const isAClass = node.type === 'ClassDeclaration'
const isArrowFunction = node.type === 'ArrowFunctionExpression'
const isAMethod = node.type === 'MethodDefinition'

const nodeName = getElementName(node, {isAClass, isAMethod, isArrowFunction})
const decorators = getDecoratorsByNode(node, {isAClass, isAMethod, isArrowFunction})
const hasDecorators = decorators?.length > 0

// Get the @Deprecated() decorator from node decorators
const deprecatedDecoratorNode =
hasDecorators && decorators?.find(decorator => decorator?.expression?.callee?.name === 'Deprecated')

if (!deprecatedDecoratorNode) return

const deprecatedDecoratorArguments = deprecatedDecoratorNode.expression?.arguments
// The decorator must have 1 argument and it should be an object
const hasArgument = deprecatedDecoratorArguments.length === 1
const argumentDecorator = hasArgument && deprecatedDecoratorArguments[0]
const isObjectExpression = hasArgument && argumentDecorator.type === 'ObjectExpression'
const argumentsAreInvalid = !hasArgument || !isObjectExpression

// Get decorator arguments: key and message
const keyProperty = !argumentsAreInvalid && argumentDecorator.properties?.find(prop => prop?.key?.name === 'key')
const messageProperty =
!argumentsAreInvalid && argumentDecorator.properties?.find(prop => prop?.key?.name === 'message')

const elementMessageName = getElementMessageName(nodeName, {isAClass, isAMethod, isArrowFunction})

// RULE: Decorator must have 1 argument as an object with Key and Message properties
if (argumentsAreInvalid || (!keyProperty && !messageProperty)) {
context.report({
node: deprecatedDecoratorNode,
messageId: 'notFoundDecoratorArgumentError',
*fix(fixer) {
yield fixer.insertTextBefore(
deprecatedDecoratorNode,
`\n @Deprecated({key: '${nodeName}', message: 'The ${elementMessageName} is deprecated.'})`
)
yield fixer.remove(deprecatedDecoratorNode)
}
})
return
}

// RULE: Decorator must have a key property and generates it if it doesn't exist
if (!keyProperty && messageProperty) {
context.report({
node: deprecatedDecoratorNode,
messageId: 'notFoundKeyDecoratorArgumentError',
*fix(fixer) {
yield fixer.insertTextBefore(
deprecatedDecoratorNode,
`\n @Deprecated({key: '${nodeName}', message: '${messageProperty.value.value}'})`
)
yield fixer.remove(deprecatedDecoratorNode)
}
})
}

// RULE: Decorator must have a message property and generates it if it doesn't exist
if (keyProperty && !messageProperty) {
context.report({
node: deprecatedDecoratorNode,
messageId: 'notFoundMessageDecoratorArgumentError',
*fix(fixer) {
yield fixer.insertTextBefore(
deprecatedDecoratorNode,
`\n @Deprecated({key: '${keyProperty.value.value}', message: 'The ${elementMessageName} function is deprecated.'})`
)
yield fixer.remove(deprecatedDecoratorNode)
}
})
}
}

return {
ClassDeclaration: ruleRunner,
MethodDefinition: ruleRunner,
ArrowFunctionExpression: ruleRunner
}
}
}
76 changes: 76 additions & 0 deletions packages/eslint-plugin-sui/src/utils/decorators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
function getDecoratorsByNode(node, {isAClass, isAMethod, isArrowFunction}) {
if (isAClass) {
return node.decorators
}

if (isArrowFunction) {
const methodNode = node.parent
return methodNode.decorators ?? []
}

if (isAMethod) {
return node.decorators ?? []
}

return []
}

function getElementName(node, {isAClass, isAMethod, isArrowFunction}) {
if (isAClass) {
const className = node.id?.name ?? 'UnknownClass'
return `${className}`
}

if (isArrowFunction) {
const methodNode = node.parent
const classNode = methodNode?.parent?.parent
const className = classNode.id?.name ?? 'UnknownClass'
const methodName = methodNode.key?.name ?? 'UnknownMethod'

return `${className}.${methodName}`
}

if (isAMethod) {
const classNode = node.parent?.parent
const className = classNode.id?.name ?? 'UnknownClass'
const methodName = node.key?.name ?? 'UnknownMethod'

return `${className}.${methodName}`
}

return 'unknown'
}

function getElementMessageName(elementName, {isAClass, isAMethod, isArrowFunction}) {
if (isAClass) {
return `class ${elementName}`
}

if (isAMethod || isArrowFunction) {
return `method ${elementName}`
}

return 'Unknown'
}

function remarkElement(node, {isAClass, isAMethod, isArrowFunction}) {
if (isAClass) {
return node.id
}

if (isArrowFunction) {
const methodNode = node.parent
return methodNode.key
}

if (isAMethod) {
return node.key
}

return node
}

module.exports.getDecoratorsByNode = getDecoratorsByNode
module.exports.getElementMessageName = getElementMessageName
module.exports.getElementName = getElementName
module.exports.remarkElement = remarkElement
46 changes: 46 additions & 0 deletions packages/sui-decorators/src/decorators/deprecated/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import isNode from '../../helpers/isNode.js'

const noop = () => {}

const getListener = () => {
const listener = isNode ? global.__SUI_DECORATOR_DEPRECATED_REPORTER__ : window.__SUI_DECORATOR_DEPRECATED_REPORTER__
return listener || noop
}

const _runner = ({instance, original, config} = {}) => {
return function (...args) {
const listener = getListener()
listener(config)

const {message} = config

if (process.env.NODE_ENV !== 'production') {
console.warn(message)
}

return original.apply(instance, args)
}
}

export function Deprecated(config = {message: ''}) {
return function (target, fnName, descriptor) {
const {value: fn, configurable, enumerable} = descriptor

return Object.assign(
{},
{
configurable,
enumerable,
value(...args) {
const _fnRunner = _runner({
instance: this,
original: fn,
config
})

return _fnRunner.apply(this, args)
}
}
)
}
}
3 changes: 2 additions & 1 deletion packages/sui-decorators/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {cache, invalidateCache} from './decorators/cache/index.js'
import {Deprecated} from './decorators/deprecated/index.js'
import inlineError from './decorators/error.js'
import streamify from './decorators/streamify.js'
import tracer from './decorators/tracer/index.js'

export {cache, invalidateCache, streamify, inlineError, tracer}
export {cache, Deprecated, invalidateCache, streamify, inlineError, tracer}
48 changes: 48 additions & 0 deletions packages/sui-decorators/test/browser/DeprecatedSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {expect} from 'chai'
import sinon from 'sinon'

import {Deprecated} from '../../src/decorators/deprecated/index.js'

describe('Deprecated decorator', () => {
beforeEach(() => {
sinon.stub(console, 'warn')
window.__SUI_DECORATOR_DEPRECATED_REPORTER__ = undefined
})

afterEach(() => {
sinon.restore()
})

it('should exist', () => {
expect(Deprecated).to.exist
expect(Deprecated).to.be.a('function')
})

it('should write a console.warn into console', async () => {
class Buzz {
@Deprecated({key: 'returnASuccessPromise', message: 'This method is deprecated'})
returnASuccessPromise() {
return Promise.resolve(true)
}
}

const buzz = new Buzz()
expect(await buzz.returnASuccessPromise()).to.be.eql(true)
expect(console.warn.calledOnce).to.be.true
})

it('should call to listener', async () => {
window.__SUI_DECORATOR_DEPRECATED_REPORTER__ = sinon.spy()

class Buzz {
@Deprecated({key: 'returnASuccessPromise', message: 'This method is deprecated'})
returnASuccessPromise() {
return Promise.resolve(true)
}
}

const buzz = new Buzz()
expect(await buzz.returnASuccessPromise()).to.be.eql(true)
expect(window.__SUI_DECORATOR_DEPRECATED_REPORTER__.calledOnce).to.be.true
})
})
Loading

0 comments on commit c8e2bce

Please sign in to comment.