From f210c46717117b4132a3586a2e2fe2548a4a51d2 Mon Sep 17 00:00:00 2001 From: Carlos Villuendas Zambrana Date: Tue, 23 Apr 2024 15:45:14 +0200 Subject: [PATCH 1/3] feat(packages/eslint-plugin-sui): Add rule to mark any uso off commonjs syntax in our code --- packages/eslint-plugin-sui/src/index.js | 4 +- .../eslint-plugin-sui/src/rules/commonjs.js | 110 ++++++++++++++ .../src/rules/forbidden-require.js | 46 ------ .../eslint-plugin-sui/test/server/commonjs.js | 141 ++++++++++++++++++ 4 files changed, 254 insertions(+), 47 deletions(-) create mode 100644 packages/eslint-plugin-sui/src/rules/commonjs.js delete mode 100644 packages/eslint-plugin-sui/src/rules/forbidden-require.js create mode 100644 packages/eslint-plugin-sui/test/server/commonjs.js diff --git a/packages/eslint-plugin-sui/src/index.js b/packages/eslint-plugin-sui/src/index.js index 84d25af60..daefc476c 100644 --- a/packages/eslint-plugin-sui/src/index.js +++ b/packages/eslint-plugin-sui/src/index.js @@ -1,5 +1,6 @@ const FactoryPattern = require('./rules/factory-pattern.js') const SerializeDeserialize = require('./rules/serialize-deserialize.js') +const CommonJS = require('./rules/commonjs.js') // ------------------------------------------------------------------------------ // Plugin Definition @@ -9,6 +10,7 @@ const SerializeDeserialize = require('./rules/serialize-deserialize.js') module.exports = { rules: { 'factory-pattern': FactoryPattern, - 'serialize-deserialize': SerializeDeserialize + 'serialize-deserialize': SerializeDeserialize, + commonjs: CommonJS } } diff --git a/packages/eslint-plugin-sui/src/rules/commonjs.js b/packages/eslint-plugin-sui/src/rules/commonjs.js new file mode 100644 index 000000000..2e3820d8e --- /dev/null +++ b/packages/eslint-plugin-sui/src/rules/commonjs.js @@ -0,0 +1,110 @@ +/** + * @fileoverview Ensure your code is not using CommonJS signatures like module.exports or moduel.exports.foo or require() or require.resolve() + */ +'use strict' + +const dedent = require('string-dedent') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Ensure that your code is using ems over commonjs modules', + recommended: true, + url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements' + }, + fixable: null, + schema: [], + messages: { + forbiddenExports: dedent` + Use module.* should be avoid. + `, + forbiddenRequires: dedent` + Use require function should be avoid. + `, + forbiddenModuleRequire: dedent` + Use module.require function should be avoid. + `, + forbiddenRequiresObjects: dedent` + Use require.cache or require.extensions or require.main should be avoid. + `, + forbiddenRequireResolve: dedent` + Use require.resolve function should be avoid. + `, + forbidden__filename: dedent` + __filename should be avoid + `, + forbidden__dirname: dedent` + __dirname should be avoid + ` + } + }, + create: function (context) { + return { + CallExpression(node) { + const isRequire = node.callee?.name === 'require' + const isResolve = node.callee?.object?.name === 'require' && node.callee?.property?.name === 'resolve' + const isModule = node.callee?.object?.name === 'module' && node.callee?.property?.name === 'require' + + isRequire && + context.report({ + node, + messageId: 'forbiddenRequires' + }) + + isResolve && + context.report({ + node, + messageId: 'forbiddenRequireResolve' + }) + + isModule && + context.report({ + node, + messageId: 'forbiddenModuleRequire' + }) + }, + MemberExpression(node) { + const isModule = + node.object?.name === 'module' && + ['children', 'exports', 'filename', 'id', 'isPreloading', 'loaded', 'parent', 'path', 'paths'].some( + property => node.property?.name === property + ) + + const isRequire = + node.object?.name === 'require' && + ['cache', 'extensions', 'main'].some(property => node.property?.name === property) + + isModule && + context.report({ + node, + messageId: 'forbiddenExports' + }) + + isRequire && + context.report({ + node, + messageId: 'forbiddenRequiresObjects' + }) + }, + Identifier(node) { + node.name === '__filename' && + context.report({ + node, + messageId: 'forbidden__filename' + }) + + node.name === '__dirname' && + context.report({ + node, + messageId: 'forbidden__dirname' + }) + } + } + } +} diff --git a/packages/eslint-plugin-sui/src/rules/forbidden-require.js b/packages/eslint-plugin-sui/src/rules/forbidden-require.js deleted file mode 100644 index 5e31b845b..000000000 --- a/packages/eslint-plugin-sui/src/rules/forbidden-require.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @fileoverview Ensure that our coda doesnt have require (CJS) styles - * @author factory pattern - */ -'use strict' - -const dedent = require('string-dedent') - -// ------------------------------------------------------------------------------ -// Rule Definition -// ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - meta: { - type: 'problem', - docs: { - description: 'ensure to use only ESM (import) style', - recommended: true, - url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements' - }, - fixable: null, - schema: [], - messages: { - badFileName: dedent``, - badClassName: dedent`` - } - }, - create: function (context) { - // variables should be defined here - - // ---------------------------------------------------------------------- - // Helpers - // ---------------------------------------------------------------------- - - // any helper functions should go here or else delete this section - - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - - return { - ClassDeclaration(node) {} - } - } -} diff --git a/packages/eslint-plugin-sui/test/server/commonjs.js b/packages/eslint-plugin-sui/test/server/commonjs.js new file mode 100644 index 000000000..24f87559b --- /dev/null +++ b/packages/eslint-plugin-sui/test/server/commonjs.js @@ -0,0 +1,141 @@ +import dedent from 'dedent' +import {RuleTester} from 'eslint' + +import rule from '../../src/rules/commonjs.js' + +// ------------------------------------------------------------------------------ +// Tests +// more info: https://eslint.org/docs/latest/integrate/nodejs-api#ruletester +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions: {ecmaVersion: 2018, sourceType: 'module'}}) +ruleTester.run('esm', rule, { + valid: [ + { + code: dedent` + class User { + static create() { return new User() } + } + ` + } + ], + + invalid: [ + { + code: dedent` + class Model { + constructor() { this.name = 'John Doe' } + } + module.exports = Model + `, + errors: [ + { + message: dedent` + Use module.* should be avoid. + ` + } + ] + }, + { + code: dedent` + const deps = require('my-dep') + class Config { + static create() { + return {API_URL: 'google.com'} + } + } + `, + errors: [ + { + message: dedent` + Use require function should be avoid. + ` + } + ] + }, + { + code: dedent` + const path = require.resolve('my-dep') + `, + errors: [ + { + message: dedent` + Use require.resolve function should be avoid. + ` + } + ] + }, + { + code: dedent` + console.log(require.main) + `, + errors: [ + { + message: dedent` + Use require.cache or require.extensions or require.main should be avoid. + ` + } + ] + }, + { + code: dedent` + console.log(require.cache) + `, + errors: [ + { + message: dedent` + Use require.cache or require.extensions or require.main should be avoid. + ` + } + ] + }, + { + code: dedent` + console.log(require.extensions) + `, + errors: [ + { + message: dedent` + Use require.cache or require.extensions or require.main should be avoid. + ` + } + ] + }, + { + code: dedent` + console.log(__dirname) + `, + errors: [ + { + message: dedent` + __dirname should be avoid + ` + } + ] + }, + { + code: dedent` + console.log(__filename) + `, + errors: [ + { + message: dedent` + __filename should be avoid + ` + } + ] + }, + { + code: dedent` + module.require(id) + `, + errors: [ + { + message: dedent` + Use module.require function should be avoid. + ` + } + ] + } + ] +}) From 86ef4a25ee642b6a1fe2cdb134958a342f13edcf Mon Sep 17 00:00:00 2001 From: Carlos Villuendas Zambrana Date: Thu, 25 Apr 2024 11:50:52 +0200 Subject: [PATCH 2/3] feat(packages/eslint-plugin-sui): add basic decorators rules for UseCases --- packages/eslint-plugin-sui/src/index.js | 4 +- .../eslint-plugin-sui/src/rules/decorators.js | 86 ++++++++++++++ .../test/server/decorators.js | 109 ++++++++++++++++++ 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin-sui/src/rules/decorators.js create mode 100644 packages/eslint-plugin-sui/test/server/decorators.js diff --git a/packages/eslint-plugin-sui/src/index.js b/packages/eslint-plugin-sui/src/index.js index daefc476c..3e33063fa 100644 --- a/packages/eslint-plugin-sui/src/index.js +++ b/packages/eslint-plugin-sui/src/index.js @@ -1,6 +1,7 @@ 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') // ------------------------------------------------------------------------------ // Plugin Definition @@ -11,6 +12,7 @@ module.exports = { rules: { 'factory-pattern': FactoryPattern, 'serialize-deserialize': SerializeDeserialize, - commonjs: CommonJS + commonjs: CommonJS, + decorators: Decorators } } diff --git a/packages/eslint-plugin-sui/src/rules/decorators.js b/packages/eslint-plugin-sui/src/rules/decorators.js new file mode 100644 index 000000000..9c524de80 --- /dev/null +++ b/packages/eslint-plugin-sui/src/rules/decorators.js @@ -0,0 +1,86 @@ +/** + * @fileoverview Ensure that at least all your UseCases are using @inlineError and @tracer decorator from sui + */ +'use strict' + +const dedent = require('string-dedent') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Ensure that at least all your UseCases are using @inlineError and @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 be always the first + ` + } + }, + create: function (context) { + return { + MethodDefinition(node) { + 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 && + context.report({ + node: node.key, + messageId: 'missingTracer' + }) + + tracerNode && + !isTracerCalledWithClassName && + context.report({ + node: tracerNode, + messageId: 'tracerMissCall', + fix(fixer) { + return fixer.replaceText(tracerNode.expression, `tracer({metric: '${className}#${node.key?.name}'})`) + } + }) + } + } + } +} diff --git a/packages/eslint-plugin-sui/test/server/decorators.js b/packages/eslint-plugin-sui/test/server/decorators.js new file mode 100644 index 000000000..d52ea4ee0 --- /dev/null +++ b/packages/eslint-plugin-sui/test/server/decorators.js @@ -0,0 +1,109 @@ +import dedent from 'dedent' +import {RuleTester} from 'eslint' + +import rule from '../../src/rules/decorators.js' + +// ------------------------------------------------------------------------------ +// Tests +// more info: https://eslint.org/docs/latest/integrate/nodejs-api#ruletester +// ------------------------------------------------------------------------------ + +const resolvedBabelPresetSui = require.resolve('babel-preset-sui') +const parser = require.resolve('@babel/eslint-parser') + +const ruleTester = new RuleTester({parser, parserOptions: {babelOptions: {configFile: resolvedBabelPresetSui}}}) +ruleTester.run('decorators', rule, { + valid: [ + { + code: dedent` + class MyUseCase extends UseCase { + + @tracer({metric: 'MyUseCase#execute'}) + @inlineError + execute(){} + } + ` + } + ], + + invalid: [ + { + code: dedent` + class MyUseCase extends UseCase { + + execute(){} + } + `, + errors: [ + { + message: dedent` + All our UseCases must have an @inlineError decorator. + ` + }, + { + message: dedent` + All our UseCases must have a @tracer() decorator. + ` + } + ] + }, + { + code: dedent` + class MyUseCase extends UseCase { + + @inlineError + execute(){} + } + `, + errors: [ + { + message: dedent` + All our UseCases must have a @tracer() decorator. + ` + } + ] + }, + { + code: dedent` + class MyUseCase extends UseCase { + + @tracer() + @inlineError + execute(){} + } + `, + output: dedent` + class MyUseCase extends UseCase { + + @tracer({metric: 'MyUseCase#execute'}) + @inlineError + execute(){} + } + `, + errors: [ + { + message: dedent` + Your tracer decorator should be call always with the name of your class + ` + } + ] + }, + { + code: dedent` + class MyUseCase extends UseCase { + + @inlineError + @tracer({metric: 'MyUseCase#execute'}) + execute(){} + } + `, + errors: [ + { + message: dedent` + the inlineError decorator should be always the first + ` + } + ] + } + ] +}) From ea666e0495246e15d209aac2a8ad1acc477430a1 Mon Sep 17 00:00:00 2001 From: Carlos Villuendas Zambrana Date: Thu, 25 Apr 2024 14:23:17 +0200 Subject: [PATCH 3/3] feat(packages/eslint-plugin-sui): Allow require from ESM module --- .../eslint-plugin-sui/src/rules/commonjs.js | 8 ++++++++ .../eslint-plugin-sui/test/server/commonjs.js | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-sui/src/rules/commonjs.js b/packages/eslint-plugin-sui/src/rules/commonjs.js index 2e3820d8e..88d6dacf0 100644 --- a/packages/eslint-plugin-sui/src/rules/commonjs.js +++ b/packages/eslint-plugin-sui/src/rules/commonjs.js @@ -51,7 +51,15 @@ module.exports = { const isResolve = node.callee?.object?.name === 'require' && node.callee?.property?.name === 'resolve' const isModule = node.callee?.object?.name === 'module' && node.callee?.property?.name === 'require' + const isRequireFormCreateRequire = node.parent?.parent?.body + ?.filter(node => node.type === 'ImportDeclaration') + ?.some( + node => + node.source?.value === 'module' && node.specifiers?.some(spec => spec.imported?.name === 'createRequire') + ) + isRequire && + !isRequireFormCreateRequire && context.report({ node, messageId: 'forbiddenRequires' diff --git a/packages/eslint-plugin-sui/test/server/commonjs.js b/packages/eslint-plugin-sui/test/server/commonjs.js index 24f87559b..a527b088b 100644 --- a/packages/eslint-plugin-sui/test/server/commonjs.js +++ b/packages/eslint-plugin-sui/test/server/commonjs.js @@ -3,16 +3,31 @@ import {RuleTester} from 'eslint' import rule from '../../src/rules/commonjs.js' +const resolvedBabelPresetSui = require.resolve('babel-preset-sui') +const parser = require.resolve('@babel/eslint-parser') + // ------------------------------------------------------------------------------ // Tests // more info: https://eslint.org/docs/latest/integrate/nodejs-api#ruletester // ------------------------------------------------------------------------------ -const ruleTester = new RuleTester({parserOptions: {ecmaVersion: 2018, sourceType: 'module'}}) +const ruleTester = new RuleTester({parser, parserOptions: {babelOptions: {configFile: resolvedBabelPresetSui}}}) ruleTester.run('esm', rule, { valid: [ { code: dedent` + class User { + static create() { return new User() } + } + ` + }, + { + code: dedent` + import { createRequire } from "module" + const require = createRequire(import.meta.url) + + require('whatever') + class User { static create() { return new User() } }