diff --git a/package.json b/package.json index f0951fa56..e41587415 100644 --- a/package.json +++ b/package.json @@ -79,4 +79,4 @@ "stylelint": { "extends": "./node_modules/@s-ui/lint/stylelint.config.js" } -} \ No newline at end of file +} diff --git a/packages/eslint-plugin-sui/README.md b/packages/eslint-plugin-sui/README.md new file mode 100644 index 000000000..07481eb4d --- /dev/null +++ b/packages/eslint-plugin-sui/README.md @@ -0,0 +1,58 @@ +# eslint-plugin-sui + +Set of sui lint rules + +## Installation + +You'll first need to install [ESLint](https://eslint.org/): + +```sh +npm i eslint --save-dev +``` + +Next, install `eslint-plugin-sui`: + +```sh +npm install eslint-plugin-sui --save-dev +``` + +## Usage + +Add `sui` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: + +```json +{ + "plugins": [ + "sui" + ] +} +``` + + +Then configure the rules you want to use under the rules section. + +```json +{ + "rules": { + "sui/rule-name": 2 + } +} +``` + + + +## Configurations + + +TODO: Run eslint-doc-generator to generate the configs list (or delete this section if no configs are offered). + + + + +## Rules + + +TODO: Run eslint-doc-generator to generate the rules list. + + + diff --git a/packages/eslint-plugin-sui/docs/rules/factory-pattern.md b/packages/eslint-plugin-sui/docs/rules/factory-pattern.md new file mode 100644 index 000000000..c6699910d --- /dev/null +++ b/packages/eslint-plugin-sui/docs/rules/factory-pattern.md @@ -0,0 +1,35 @@ +# Ensure that our classes are using the convetion of has a static create method as factory. (`factory-pattern`) + +Please describe the origin of the rule here. + +## Rule Details + +This rule aims to... + +Examples of **incorrect** code for this rule: + +```js + +// fill me in + +``` + +Examples of **correct** code for this rule: + +```js + +// fill me in + +``` + +### Options + +If there are any options, describe them here. Otherwise, delete this section. + +## When Not To Use It + +Give a short description of when it would be appropriate to turn off this rule. + +## Further Reading + +If there are other links that describe the issue this rule addresses, please include them here in a bulleted list. diff --git a/packages/eslint-plugin-sui/package.json b/packages/eslint-plugin-sui/package.json new file mode 100644 index 000000000..e6f9ccb1b --- /dev/null +++ b/packages/eslint-plugin-sui/package.json @@ -0,0 +1,38 @@ +{ + "name": "eslint-plugin-sui", + "version": "1.0.0-beta.9", + "description": "Set of sui lint rules", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin" + ], + "author": "Sui", + "main": "./src/index.js", + "exports": "./src/index.js", + "scripts": { + "lint": "npx npm-run-all \"lint:*\"", + "lint:eslint-docs": "npx npm-run-all \"update:eslint-docs -- --check\"", + "lint:js": "npx eslint .", + "test": "npx sui-test server", + "update:eslint-docs": "npx eslint-doc-generator" + }, + "dependencies": { + "requireindex": "1.2.0", + "string-dedent": "3.0.1" + }, + "devDependencies": { + "@s-ui/test": "8", + "eslint": "8.19.0", + "eslint-doc-generator": "1.0.0", + "eslint-plugin-eslint-plugin": "5.0.0", + "eslint-plugin-node": "11.1.0" + }, + "engines": { + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" + }, + "peerDependencies": { + "eslint": ">=7" + }, + "license": "ISC" +} diff --git a/packages/eslint-plugin-sui/src/index.js b/packages/eslint-plugin-sui/src/index.js new file mode 100644 index 000000000..84d25af60 --- /dev/null +++ b/packages/eslint-plugin-sui/src/index.js @@ -0,0 +1,14 @@ +const FactoryPattern = require('./rules/factory-pattern.js') +const SerializeDeserialize = require('./rules/serialize-deserialize.js') + +// ------------------------------------------------------------------------------ +// Plugin Definition +// ------------------------------------------------------------------------------ + +// import all rules in lib/rules +module.exports = { + rules: { + 'factory-pattern': FactoryPattern, + 'serialize-deserialize': SerializeDeserialize + } +} diff --git a/packages/eslint-plugin-sui/src/rules/factory-pattern.js b/packages/eslint-plugin-sui/src/rules/factory-pattern.js new file mode 100644 index 000000000..51168657b --- /dev/null +++ b/packages/eslint-plugin-sui/src/rules/factory-pattern.js @@ -0,0 +1,65 @@ +/** + * @fileoverview Ensure that our classes are using the convetion of has a static create method as factory. + * @author factory pattern + */ +'use strict' + +const dedent = require('string-dedent') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'warning', + docs: { + description: 'ensure to define at least one factory function', + recommended: true, + url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements' + }, + fixable: null, + schema: [], + messages: { + notFoundFactoryFunction: dedent` + You have to define at least one static function that return an instance of your class. + Avoid to use the 'new' keyword directly in your code. + Use always a factory function + ` + } + }, + create: function (context) { + // variables should be defined here + + // ---------------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------------- + + // any helper functions should go here or else delete this section + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return { + ClassDeclaration(node) { + const hasStaticFactoryMethod = Boolean( + node.body?.body?.find(methodDefinition => { + return ( + methodDefinition.static && + methodDefinition.value?.body?.body?.find?.(body => body.type === 'ReturnStatement')?.argument?.callee?.name === node?.id?.name // eslint-disable-line + ) + }) + ) + + if (!hasStaticFactoryMethod) { + context.report({ + node: node?.id ?? node.superClass ?? node, + messageId: 'notFoundFactoryFunction' + }) + } + } + } + } +} diff --git a/packages/eslint-plugin-sui/src/rules/forbidden-require.js b/packages/eslint-plugin-sui/src/rules/forbidden-require.js new file mode 100644 index 000000000..5e31b845b --- /dev/null +++ b/packages/eslint-plugin-sui/src/rules/forbidden-require.js @@ -0,0 +1,46 @@ +/** + * @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/src/rules/function-named-parameters.js b/packages/eslint-plugin-sui/src/rules/function-named-parameters.js new file mode 100644 index 000000000..e4327df59 --- /dev/null +++ b/packages/eslint-plugin-sui/src/rules/function-named-parameters.js @@ -0,0 +1,46 @@ +/** + * @fileoverview Ensure that our function are using always named parameters + * @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 named parameters', + 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/src/rules/naming-convention.js b/packages/eslint-plugin-sui/src/rules/naming-convention.js new file mode 100644 index 000000000..7da81c742 --- /dev/null +++ b/packages/eslint-plugin-sui/src/rules/naming-convention.js @@ -0,0 +1,46 @@ +/** + * @fileoverview Ensure that our classes are using the naming convention for UseCases, Services and Repositories + * @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 a proper naming convention', + 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/src/rules/serialize-deserialize.js b/packages/eslint-plugin-sui/src/rules/serialize-deserialize.js new file mode 100644 index 000000000..33c6c2232 --- /dev/null +++ b/packages/eslint-plugin-sui/src/rules/serialize-deserialize.js @@ -0,0 +1,104 @@ +/** + * @fileoverview ensure entity create - toJSON + * @creator david.lacedonia@adevinta.com + */ +'use strict' +const dedent = require('string-dedent') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'ensure entity create - toJSON', + recommended: false, + url: 'https://github.mpi-internal.com/scmspain/es-td-agreements/blob/master/30-Frontend/00-agreements' + }, + fixable: null, + schema: [], + messages: { + toJSONProperties: 'Missing toJSON properties ({{props}})', + invalidTOJSONProperties: 'toJSON should return an object', + missingToJSONMethod: dedent` + If your class has a 'static create' method. You have to define a 'toJSON' method too. + The output of the 'toJSON' should be the same as the input of your 'static create' method + `, + missingCreateMethod: dedent` + If your class has a 'toJSON' method. You have to define a 'static create' method too. + The output of the 'toJSON' should be the same as the input of your 'static create' method + `, + forbiddenSpreadElements: dedent` + Spread operation are not allowed as part of the toJSON function. + The output of the 'toJSON' should be the same as the input of your 'static create' method + ` + } + }, + + create(context) { + return { + ClassDeclaration(node) { + const create = node.body.body.find(i => i.key.name === 'create') + const toJSON = node.body.body.find(i => i.key.name === 'toJSON') + const className = node?.id?.name ?? '' + + if (['UseCase', 'Service', 'Repository'].some(allowWord => className.includes(allowWord))) return // eslint-disable-line + + if (!create && !toJSON) return + + if (create && !toJSON) + return context.report({ + node: create.key, + messageId: 'missingToJSONMethod' + }) + + if (toJSON && !create) + return context.report({ + node: toJSON.key, + messageId: 'missingCreateMethod' + }) + + let createParams = create.value.params[0] || {properties: []} + if (createParams.left) { + createParams = createParams.left + } + + const createProperties = createParams.properties + const toJSONProperties = toJSON.value.body.body?.find(node => node.type === 'ReturnStatement')?.argument + ?.properties + + const spreadElement = toJSONProperties?.find(node => node.type === 'SpreadElement') + if (spreadElement) { + return context.report({ + node: spreadElement, + messageId: 'forbiddenSpreadElements' + }) + } + + if (!toJSONProperties) { + return context.report({ + node: toJSON.key, + messageId: 'invalidTOJSONProperties' + }) + } + + const createProps = createProperties.map(i => i.key.name) + const toJSONProps = toJSONProperties.map(i => i.key.name) + + const missingToJSONProps = createProps.filter(p => !toJSONProps.find(e => e === p)) + if (missingToJSONProps.length) { + context.report({ + node: toJSON.key, + messageId: 'toJSONProperties', + data: { + props: missingToJSONProps.join(', ') + } + }) + } + } + } + } +} diff --git a/packages/eslint-plugin-sui/test/server/factory-pattern.js b/packages/eslint-plugin-sui/test/server/factory-pattern.js new file mode 100644 index 000000000..cbde9a821 --- /dev/null +++ b/packages/eslint-plugin-sui/test/server/factory-pattern.js @@ -0,0 +1,90 @@ +import dedent from 'dedent' +import {RuleTester} from 'eslint' + +import rule from '../../src/rules/factory-pattern.js' + +// ------------------------------------------------------------------------------ +// Tests +// more info: https://eslint.org/docs/latest/integrate/nodejs-api#ruletester +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions: {ecmaVersion: 2018, sourceType: 'module'}}) +ruleTester.run('factory-pattern', rule, { + valid: [ + { + code: dedent` + class User { + static create() { return new User() } + } + ` + } + ], + + invalid: [ + { + code: dedent` + class Model { + constructor() { this.name = 'John Doe' } + }`, + errors: [ + { + message: dedent` + You have to define at least one static function that return an instance of your class. + Avoid to use the 'new' keyword directly in your code. + Use always a factory function + ` + } + ] + }, + { + code: dedent` + class Config { + static create() { + return {API_URL: 'google.com'} + } + } + `, + errors: [ + { + message: dedent` + You have to define at least one static function that return an instance of your class. + Avoid to use the 'new' keyword directly in your code. + Use always a factory function + ` + } + ] + }, + { + code: dedent` + class Config { + static create() { + return () => {} + } + } + `, + errors: [ + { + message: dedent` + You have to define at least one static function that return an instance of your class. + Avoid to use the 'new' keyword directly in your code. + Use always a factory function + ` + } + ] + }, + { + code: dedent` + export default class extends Model {} + `, + errors: [ + { + message: dedent` + You have to define at least one static function that return an instance of your class. + Avoid to use the 'new' keyword directly in your code. + Use always a factory function + ` + } + ] + } + ] +}) diff --git a/packages/eslint-plugin-sui/test/server/serialize-deserialize.js b/packages/eslint-plugin-sui/test/server/serialize-deserialize.js new file mode 100644 index 000000000..6ff0e8dbb --- /dev/null +++ b/packages/eslint-plugin-sui/test/server/serialize-deserialize.js @@ -0,0 +1,141 @@ +import dedent from 'dedent' +import {RuleTester} from 'eslint' + +import rule from '../../src/rules/serialize-deserialize.js' + +// ------------------------------------------------------------------------------ +// Tests +// more info: https://eslint.org/docs/latest/integrate/nodejs-api#ruletester +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions: {ecmaVersion: 2018}}) +ruleTester.run('serialize-deserialize', rule, { + valid: [ + { + code: dedent` + class User { + static create({id, name}) { return new User(id, name) } + constructor(id, name) { + this.id = id + this.name = name + } + toJSON() { + return { + id: this.id, + name: this.name + } + } + } + ` + }, + { + code: dedent` + class User { + static create({id, name}) { return new User(id, name) } + constructor(id, name) { + this.id = id + this.name = name + } + toJSON() { + const name="John" + const surname="Doe" + + return { + id: this.id, + name: this.name + } + } + } + ` + } + ], + + invalid: [ + { + code: dedent` + class User { + static create({id, name}) { return new User(id, name) } + constructor(id, name) { + this.id = id + this.name = name + } + toJSON() { + return this.id + } + } + `, + errors: [ + { + message: dedent`toJSON should return an object` + } + ] + }, + { + code: dedent` + class User { + constructor(id, name) { + this.id = id + this.name = name + } + toJSON() { + return { + Noid: this.id, + Noname: this.name + } + } + } + `, + errors: [ + { + message: dedent` + If your class has a 'toJSON' method. You have to define a 'static create' method too. + The output of the 'toJSON' should be the same as the input of your 'static create' method + ` + } + ] + }, + { + code: dedent` + class User { + static create({id, name}) { return new User(id, name) } + constructor(id, name) { + this.id = id + this.name = name + } + toJSON() { + return { + Noid: this.id, + Noname: this.name + } + } + } + `, + errors: [{message: 'Missing toJSON properties (id, name)'}] + }, + { + code: dedent` + class User { + static create({id, name}) { return new User(id, name) } + constructor(id, name) { + this.id = id + this.name = name + } + toJSON() { + return { + Noid: this.id, + ...this.user.toJSON() + } + } + } + `, + errors: [ + { + message: dedent` + Spread operation are not allowed as part of the toJSON function. + The output of the 'toJSON' should be the same as the input of your 'static create' method + ` + } + ] + } + ] +}) diff --git a/packages/lint-repository-sui/README.md b/packages/lint-repository-sui/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/lint-repository-sui/package.json b/packages/lint-repository-sui/package.json new file mode 100644 index 000000000..8bbd986c7 --- /dev/null +++ b/packages/lint-repository-sui/package.json @@ -0,0 +1,15 @@ +{ + "name": "lint-repository-sui", + "version": "0.0.0", + "description": "Set of sui lint rules to lint a repository", + "keywords": [], + "author": "Sui", + "main": "./src/index.js", + "exports": "./src/index.js", + "scripts": { + "test": "npx sui-test server" + }, + "devDependencies": { + "@s-ui/test": "8" + } +} diff --git a/packages/lint-repository-sui/src/index.js b/packages/lint-repository-sui/src/index.js new file mode 100644 index 000000000..d1f8a51d1 --- /dev/null +++ b/packages/lint-repository-sui/src/index.js @@ -0,0 +1,20 @@ +const NodeVersion = require('./rules/node-version.js') +const ReactVersion = require('./rules/react-version.js') +const PackageLock = require('./rules/package-lock.js') +const GithubAction = require('./rules/github-action.js') +const TypeScript = require('./rules/typescript.js') + +// ------------------------------------------------------------------------------ +// Plugin Definition +// ------------------------------------------------------------------------------ + +// import all rules in lib/rules +module.exports = { + rules: { + 'node-version': NodeVersion, + 'react-version': ReactVersion, + 'package-lock': PackageLock, + 'github-action': GithubAction, + typescript: TypeScript + } +} diff --git a/packages/lint-repository-sui/src/rules/github-action.js b/packages/lint-repository-sui/src/rules/github-action.js new file mode 100644 index 000000000..5ace0db38 --- /dev/null +++ b/packages/lint-repository-sui/src/rules/github-action.js @@ -0,0 +1,71 @@ +const dedent = require('string-dedent') + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Check that your repository have properly setup the GHA to CI/CD', + recommended: true, + url: null + }, + fixable: null, + schema: [], + messages: { + missingGithubFolder: dedent` + Every project needs to have a .github/worflows folder to be able to run CI/CD in GHA. + If you are not sure about how do it, please contact with Platform Web. + `, + missingMainWorkflow: dedent` + Every project needs to have a workflow to run on master. + If you are not sure about how do it, please contact with Platform Web. + `, + missingPRWorkflow: dedent` + Every project needs to have a workflow to run on every PR. + If you are not sure about how do it, please contact with Platform Web. + ` + } + }, + reduceMonitoring: function (monitorings) { + return monitorings.reduce((acc, signal) => { + return acc && signal.value + }, true) + }, + create: function (context) { + return { + '.github/workflows': matches => { + context.monitoring(true) + }, + + '.github/**/main.yml': matches => { + context.monitoring(true) + }, + + '.github/**/pullrequest.yml': matches => { + context.monitoring(true) + }, + + missmatch: key => { + if (key === '.github/workflows') { + context.report({ + messageId: 'missingGithubFolder' + }) + context.monitoring(false, '.github/workflows') + } + + if (key === '.github/**/main.yml') { + context.report({ + messageId: 'missingMainWorkflow' + }) + context.monitoring(false, '.github/**/main.yml') + } + + if (key === '.github/**/pullrequest.yml') { + context.report({ + messageId: 'missingPRWorkflow' + }) + context.monitoring(false, '.github/**/pullrequest.yml') + } + } + } + } +} diff --git a/packages/lint-repository-sui/src/rules/node-version.js b/packages/lint-repository-sui/src/rules/node-version.js new file mode 100644 index 000000000..5b15ebbda --- /dev/null +++ b/packages/lint-repository-sui/src/rules/node-version.js @@ -0,0 +1,62 @@ +const dedent = require('string-dedent') + +const NODE_VERSION = '20' + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Check that your repository use the latest Node version', + recommended: true, + url: null + }, + fixable: null, + schema: [], + messages: { + moreThanOneNVMRC: dedent` + Your project has more than one .nvmrc file. That can be dangerous. + Please, use onle ONE in the root of your project. + If you are not sure about how do it, please contact with Platform Web. + `, + badNodeVersion: dedent` + Your current Node version is {{version}}. + Please be sure that your repository use the latest Node Version ${NODE_VERSION}. + If you are not sure about how do it, please contact with Platform Web. + `, + noNMVRCFile: dedent` + Every project have to have a .npmrc file to define the node versión. + If you are not sure about how do it, please contact with Platform Web. + ` + } + }, + create: function (context) { + return { + '.nvmrc': matches => { + if (matches.length > 1) { + context.report({ + messageId: 'moreThanOneNVMRC' + }) + return context.monitoring(0) + } + + const [nvmrcMatch] = matches + const [version] = nvmrcMatch.raw.trim().split('.') + if (version !== NODE_VERSION) { + context.report({ + messageId: 'badNodeVersion', + data: {version} + }) + } + + context.monitoring(version) + }, + + missmatch: key => { + context.report({ + messageId: 'noNMVRCFile' + }) + context.monitoring(0) + } + } + } +} diff --git a/packages/lint-repository-sui/src/rules/package-lock.js b/packages/lint-repository-sui/src/rules/package-lock.js new file mode 100644 index 000000000..657fdaca0 --- /dev/null +++ b/packages/lint-repository-sui/src/rules/package-lock.js @@ -0,0 +1,34 @@ +const dedent = require('string-dedent') + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Check that your repository have created a package-lock file', + recommended: true, + url: null + }, + fixable: null, + schema: [], + messages: { + missingPackageLock: dedent` + Every project needs to have a package-lock.json file to be used in CI/CD. + If you are not sure about how do it, please contact with Platform Web. + ` + } + }, + create: function (context) { + return { + 'package-lock.json': matches => { + context.monitoring(true) + }, + + missmatch: key => { + context.report({ + messageId: 'missingPackageLock' + }) + context.monitoring(false) + } + } + } +} diff --git a/packages/lint-repository-sui/src/rules/react-version.js b/packages/lint-repository-sui/src/rules/react-version.js new file mode 100644 index 000000000..acca08e0c --- /dev/null +++ b/packages/lint-repository-sui/src/rules/react-version.js @@ -0,0 +1,64 @@ +const dedent = require('string-dedent') + +const REACT_VERSION = '18' + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Check that your repository use the latest React version', + recommended: true, + url: null + }, + fixable: null, + schema: [], + messages: { + badReactVersion: dedent` + Please be sure that your repository use the latest React Version ${REACT_VERSION}. + Your current version is {{version}}. + If you are not sure about how do it, please contact with Platform Web. + `, + missingReactDependencie: dedent` + Your project doesnt have installed React. + Please install at least the version ${REACT_VERSION}. + If you are not sure about how do it, please contact with Platform Web. + `, + missingPackageLock: dedent` + To calculate the react version first we need to have a package-lock.json in the root + If you are not sure about how do it, please contact with Platform Web. + ` + } + }, + create: function (context) { + return { + 'package-lock.json': matches => { + const [packageLock] = matches + let version = packageLock?.parsed?.packages?.['node_modules/react']?.version + + if (!version) { + context.report({ + messageId: 'missingReactDependencie' + }) + return context.monitoring(0) + } + + version = version.split('.')[0] + + if (version !== REACT_VERSION) { + context.report({ + messageId: 'badReactVersion', + data: {version} + }) + } + return context.monitoring(version) + }, + + missmatch: key => { + context.report({ + messageId: 'missingPackageLock' + }) + context.monitoring(0) + } + } + } +} diff --git a/packages/lint-repository-sui/src/rules/typescript.js b/packages/lint-repository-sui/src/rules/typescript.js new file mode 100644 index 000000000..a2a4dba4e --- /dev/null +++ b/packages/lint-repository-sui/src/rules/typescript.js @@ -0,0 +1,80 @@ +const dedent = require('string-dedent') + +const MIN_TYPESCRIPT_VERSION = 5 + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Check that your repository has a `tsconfig.json` file', + recommended: true, + url: null + }, + fixable: null, + schema: [], + messages: { + missingTypescriptDependency: dedent` + Your project doesn't have installed TypeScript. + Please install at least the version ${MIN_TYPESCRIPT_VERSION}. + If you are not sure about it, please contact Web Platform. + `, + badTypescriptVersion: dedent` + Please be sure that your repository use the latest TypeScript version ${MIN_TYPESCRIPT_VERSION}. + Your current version is {{version}}. + If you are not sure about it, please contact Web Platform. + `, + noTSConfigFile: dedent` + Every project must have a \`tsconfig.json\` file to setup TypeScript in the project. + If you are not sure about how do it, please contact with Web Platform team. + ` + } + }, + create: function (context) { + return { + 'tsconfig.json': () => { + // TO-DO: Check TypeScript configuration is the one we expect. + + return context.monitoring(true) + }, + 'package-lock.json': matches => { + const [packageLock] = matches + let version = packageLock?.parsed?.packages?.['node_modules/typescript']?.version + + // Check if repository is defining TS as dependency. + if (!version) { + context.report({ + messageId: 'missingTypescriptDependency' + }) + + return context.monitoring(false) + } + + version = version.split('.')[0] + + // Check if repository is using minimum expected version. + if (Number.parseInt(version) < MIN_TYPESCRIPT_VERSION) { + context.report({ + messageId: 'badTypescriptVersion', + data: {version} + }) + + return context.monitoring(false) + } + + return context.monitoring(true) + }, + missmatch: () => { + context.report({ + messageId: 'noTSConfigFile' + }) + + return context.monitoring(false) + } + } + }, + reduceMonitoring: function (monitorings) { + return monitorings.reduce((acc, signal) => { + return acc && signal.value + }, true) + } +} diff --git a/packages/lint-repository-sui/test/TestHelpers.js b/packages/lint-repository-sui/test/TestHelpers.js new file mode 100644 index 000000000..ce34922d1 --- /dev/null +++ b/packages/lint-repository-sui/test/TestHelpers.js @@ -0,0 +1,64 @@ +import {expect} from 'chai' +import {stub} from 'sinon' +export class RuleTester { + id + handler + + static create(id, handler) { + return new RuleTester(id, handler) + } + + constructor(id, handler) { + this.handler = handler + this.id = id + } + + run(assertions) { + const instance = this + + Object.entries(assertions).forEach(([kind, tests]) => { + describe(`[${kind.toUpperCase()}] ${this.id}`, function () { + beforeEach(function () { + this.ctxt = { + report: stub(), + monitoring: stub() + } + }) + afterEach(function () { + this.ctxt.report.reset() + this.ctxt.monitoring.reset() + }) + + tests.forEach(assertion => { + const {monitoring, report, name, ...rest} = assertion + Object.entries(rest).forEach(([FSPattern, matches]) => { + it(name ?? FSPattern, function () { + instance.handler.create(this.ctxt)[FSPattern](matches) + monitoring && expect(this.ctxt.monitoring.calledWith(monitoring)).to.be.eql(true) + report && expect(instance._formatMessages(this.ctxt.report)).to.be.eqls(report) + expect(true).to.be.eql(true) + }) + }) + }) + }) + }) + } + + _formatMessages(stub) { + const report = stub.firstCall.firstArg + return Object.entries(report.data ?? {}).reduce((acc, [key, value]) => { + return acc.replaceAll(`{{${key}}}`, value) + }, this.handler.meta.messages[report.messageId]) + } +} + +export class MatchStub { + static create({parsed, raw}) { + return new MatchStub(parsed, raw) + } + + constructor(parsed, raw) { + this.parsed = parsed + this.raw = raw + } +} diff --git a/packages/lint-repository-sui/test/server/github-actionSpec.js b/packages/lint-repository-sui/test/server/github-actionSpec.js new file mode 100644 index 000000000..346cf8e42 --- /dev/null +++ b/packages/lint-repository-sui/test/server/github-actionSpec.js @@ -0,0 +1,53 @@ +import dedent from 'dedent' + +import handler from '../../src/rules/github-action.js' +import {RuleTester} from '../TestHelpers.js' + +RuleTester.create('github-action', handler).run({ + valid: [ + { + '.github/workflows': [], + name: 'The porject has define a worflows folder', + monitoring: true + }, + { + '.github/**/main.yml': [], + name: 'The porject has define a worflow for the main branch', + monitoring: true + }, + { + '.github/**/pullrequest.yml': [], + name: 'The porject has define a worflow for PRs', + monitoring: true + } + ], + invalid: [ + { + missmatch: '.github/workflows', + name: 'The porject has NOT define a worflows folder', + report: dedent` + Every project needs to have a .github/worflows folder to be able to run CI/CD in GHA. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: false + }, + { + missmatch: '.github/**/main.yml', + name: 'The porject has NOT define a worflow for the main branch', + report: dedent` + Every project needs to have a workflow to run on master. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: false + }, + { + missmatch: '.github/**/pullrequest.yml', + name: 'The porject has NOT define a worflow for PRs', + report: dedent` + Every project needs to have a workflow to run on every PR. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: false + } + ] +}) diff --git a/packages/lint-repository-sui/test/server/node-versionSpec.js b/packages/lint-repository-sui/test/server/node-versionSpec.js new file mode 100644 index 000000000..daf20325d --- /dev/null +++ b/packages/lint-repository-sui/test/server/node-versionSpec.js @@ -0,0 +1,44 @@ +import dedent from 'dedent' + +import handler from '../../src/rules/node-version.js' +import {MatchStub, RuleTester} from '../TestHelpers.js' + +RuleTester.create('node-version', handler).run({ + valid: [ + { + '.nvmrc': [MatchStub.create({raw: '20'})], + name: 'nvmrc Exists and has setup the version 20', + monitoring: '20' + } + ], + invalid: [ + { + '.nvmrc': [MatchStub.create({raw: '20'}), MatchStub.create({raw: 17})], + name: 'Exits more than one nvmrc file', + report: dedent` + Your project has more than one .nvmrc file. That can be dangerous. + Please, use onle ONE in the root of your project. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: 0 + }, + { + '.nvmrc': [MatchStub.create({raw: '16.1.3'})], + name: 'Exits more than one nvmrc file', + report: dedent` + Your current Node version is 16. + Please be sure that your repository use the latest Node Version 20. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: 0 + }, + { + missmatch: '', + report: dedent` + Every project have to have a .npmrc file to define the node versión. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: 0 + } + ] +}) diff --git a/packages/lint-repository-sui/test/server/package-lockSpec.js b/packages/lint-repository-sui/test/server/package-lockSpec.js new file mode 100644 index 000000000..92773f311 --- /dev/null +++ b/packages/lint-repository-sui/test/server/package-lockSpec.js @@ -0,0 +1,25 @@ +import dedent from 'dedent' + +import handler from '../../src/rules/package-lock.js' +import {RuleTester} from '../TestHelpers.js' + +RuleTester.create('package-lock', handler).run({ + valid: [ + { + 'package-lock.json': [], + name: 'Project has package-lock.json in the root folder', + monitoring: true + } + ], + invalid: [ + { + missmatch: '', + name: 'Project doesnt has package-lock in the root folder', + report: dedent` + Every project needs to have a package-lock.json file to be used in CI/CD. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: false + } + ] +}) diff --git a/packages/lint-repository-sui/test/server/react-versionSpec.js b/packages/lint-repository-sui/test/server/react-versionSpec.js new file mode 100644 index 000000000..6f3ceded4 --- /dev/null +++ b/packages/lint-repository-sui/test/server/react-versionSpec.js @@ -0,0 +1,44 @@ +import dedent from 'dedent' + +import handler from '../../src/rules/react-version.js' +import {MatchStub, RuleTester} from '../TestHelpers.js' + +RuleTester.create('react-version', handler).run({ + valid: [ + { + 'package-lock.json': [MatchStub.create({parsed: {packages: {'node_modules/react': {version: '18.0.0'}}}})], + name: 'React 18 installed', + monitoring: '18' + } + ], + invalid: [ + { + 'package-lock.json': [MatchStub.create({parsed: {packages: {}}})], + name: 'React not installed', + report: dedent` + Your project doesnt have installed React. + Please install at least the version 18. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: 0 + }, + { + 'package-lock.json': [MatchStub.create({parsed: {packages: {'node_modules/react': {version: '17.0.0'}}}})], + name: 'React wrong version', + report: dedent` + Please be sure that your repository use the latest React Version 18. + Your current version is 17. + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: '17' + }, + { + missmatch: '', + report: dedent` + To calculate the react version first we need to have a package-lock.json in the root + If you are not sure about how do it, please contact with Platform Web. + `, + monitoring: 0 + } + ] +}) diff --git a/packages/lint-repository-sui/test/server/typescriptSpec.js b/packages/lint-repository-sui/test/server/typescriptSpec.js new file mode 100644 index 000000000..41d63d86b --- /dev/null +++ b/packages/lint-repository-sui/test/server/typescriptSpec.js @@ -0,0 +1,49 @@ +import dedent from 'dedent' + +import handler from '../../src/rules/typescript.js' +import {MatchStub, RuleTester} from '../TestHelpers.js' + +RuleTester.create('typescript', handler).run({ + valid: [ + { + 'tsconfig.json': [], + name: 'File `tsconfig.json` exists', + monitoring: true + }, + { + 'package-lock.json': [MatchStub.create({parsed: {packages: {'node_modules/typescript': {version: '5.0.4'}}}})], + name: 'TypeScript version is correct', + monitoring: true + } + ], + invalid: [ + { + 'package-lock.json': [MatchStub.create({parsed: {packages: {'node_modules/typescript': {version: '4.2.0'}}}})], + name: 'TypeScript version is not correct', + report: dedent` + Please be sure that your repository use the latest TypeScript version 5. + Your current version is 4. + If you are not sure about it, please contact Web Platform. + `, + monitoring: false + }, + { + 'package-lock.json': [MatchStub.create({parsed: {packages: {}}})], + name: 'TypeScript dependency is not installed', + report: dedent` + Your project doesn't have installed TypeScript. + Please install at least the version 5. + If you are not sure about it, please contact Web Platform. + `, + monitoring: false + }, + { + missmatch: '', + report: dedent` + Every project must have a \`tsconfig.json\` file to setup TypeScript in the project. + If you are not sure about how do it, please contact with Web Platform team. + `, + monitoring: false + } + ] +}) diff --git a/packages/sui-lint/bin/sui-lint-js.js b/packages/sui-lint/bin/sui-lint-js.js index 4e2f90642..68bd23484 100755 --- a/packages/sui-lint/bin/sui-lint-js.js +++ b/packages/sui-lint/bin/sui-lint-js.js @@ -2,25 +2,32 @@ /* eslint-disable no-console */ // @ts-check +const path = require('path') +const fs = require('fs') const program = require('commander') const {checkFilesToLint, getFilesToLint, getGitIgnoredFiles, stageFilesIfRequired} = require('../src/helpers.js') const {ESLint} = require('eslint') -const config = require('../eslintrc.js') +const config = fs.existsSync(process.cwd() + '/tsconfig.json') + ? require('../eslintrc.ts.js') + : require('../eslintrc.js') program .option('--add-fixes') .option('--staged') + .option('--force-full-lint', 'force to lint all the JS files') .option('--fix', 'fix automatically problems with js files') .option('--ignore-patterns ', 'Path patterns to ignore for linting') + .option('--reporter ', 'Send results using a custom reporter') + .option('--pattern ', 'Pattern of files to lint') .parse(process.argv) -const {addFixes, fix, ignorePatterns = [], staged} = program.opts() +const {addFixes, fix, ignorePatterns = [], staged, pattern, reporter, forceFullLint} = program.opts() const {CI} = process.env const EXTENSIONS = ['js', 'jsx', 'ts', 'tsx'] const IGNORE_PATTERNS = ['lib', 'dist', 'public', 'node_modules'] -const DEFAULT_PATTERN = './' +const DEFAULT_PATTERN = pattern ?? './' const LINT_FORMATTER = 'stylish' const baseConfig = { ...config, @@ -49,7 +56,21 @@ const baseConfig = { useEslintrc: false }) - const results = await eslint.lintFiles(files) + if (forceFullLint) { + console.log('[sui-lint] force to lint all our JS files') + } + + const results = await eslint.lintFiles(!forceFullLint ? files : DEFAULT_PATTERN) + + if (reporter) { + console.log('[sui-lint] Sending stats using the reporter ', reporter) + const reporterPath = path.isAbsolute(reporter) ? reporter : path.join(process.cwd() + '/' + reporter) + console.log({reporter, isAbsolute: path.isAbsolute(reporter), reporterPath}) + const {JSReporter} = await import(reporterPath) + const reportered = await JSReporter.create() + await reportered.map(results).send() + console.log('[sui-lint] All your stats has been sent', reporter) + } if (fix) { await ESLint.outputFixes(results) diff --git a/packages/sui-lint/bin/sui-lint-repository.js b/packages/sui-lint/bin/sui-lint-repository.js new file mode 100755 index 000000000..86f96d144 --- /dev/null +++ b/packages/sui-lint/bin/sui-lint-repository.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +const path = require('path') + +const program = require('commander') +const {RepositoryLinter} = require('../src/RepositoryLinter') + +program + .option('--reporter ', 'Send results to DD using sui-logger') + .option('--output-json ', 'Print messages errors as JSON. Default is a table') + .parse(process.argv) + +const {reporter, outputJson} = program.opts() + +;(async function main() { + const linter = RepositoryLinter.create() + const results = await linter.lint() + + if (outputJson) console.log('\n\n') + if (outputJson) { + results.logJSON() + } else { + results.logTable() + } + if (outputJson) console.log('\n\n') + + if (reporter) { + console.log('\n[sui-lint] Sending stats using the reporter\n\n', reporter) + const reporterPath = path.isAbsolute(reporter) ? reporter : path.join(process.cwd() + '/' + reporter) + console.log({reporter, isAbsolute: path.isAbsolute(reporter), reporterPath}) + const {RepositoryReporter} = await import(reporterPath) + + const reportered = RepositoryReporter.create() + await reportered.map(results).send() + results.logMonitorings() + } +})().catch(error => { + process.exitCode = 1 + console.error('[sui-lint]', error) +}) diff --git a/packages/sui-lint/bin/sui-lint.js b/packages/sui-lint/bin/sui-lint.js index 78c90c0aa..12bd4bbb9 100755 --- a/packages/sui-lint/bin/sui-lint.js +++ b/packages/sui-lint/bin/sui-lint.js @@ -4,6 +4,9 @@ const {version} = require('../package.json') program.version(version, ' --version') -program.command('js', 'lint javascript files').command('sass', 'lint sass files') +program + .command('js', 'lint javascript files') + .command('sass', 'lint sass files') + .command('repository', 'lint repository structure') program.parse(process.argv) diff --git a/packages/sui-lint/eslintrc.js b/packages/sui-lint/eslintrc.js index c247117c7..02ee6aa3c 100644 --- a/packages/sui-lint/eslintrc.js +++ b/packages/sui-lint/eslintrc.js @@ -201,6 +201,14 @@ module.exports = { 'react/jsx-no-bind': RULES.OFF }, overrides: [ + // { + // files: ['**/domain/src/**'], + // plugins: ['sui'], + // rules: { + // 'sui/factory-pattern': RULES.WARNING, + // 'sui/serialize-deserialize': RULES.WARNING + // } + // }, { files: ['**/*.+(ts|tsx)'], extends: ['standard-with-typescript'], diff --git a/packages/sui-lint/eslintrc.ts.js b/packages/sui-lint/eslintrc.ts.js new file mode 100644 index 000000000..985ac0dda --- /dev/null +++ b/packages/sui-lint/eslintrc.ts.js @@ -0,0 +1,231 @@ +const prettierOptions = require('./.prettierrc.js') + +const RULES = { + OFF: 0, + WARNING: 1, + ERROR: 2 +} + +const REACT_RULES = { + 'react-hooks/exhaustive-deps': RULES.WARNING, // Checks effect dependencies + 'react-hooks/rules-of-hooks': RULES.ERROR, // Checks rules of Hooks + 'react/default-props-match-prop-types': RULES.WARNING, + 'react/jsx-handler-names': RULES.WARNING, + 'react/jsx-no-bind': RULES.WARNING, + 'react/jsx-no-duplicate-props': [RULES.WARNING, {ignoreCase: true}], + 'react/jsx-no-undef': RULES.WARNING, + 'react/jsx-pascal-case': [ + RULES.WARNING, + { + allowAllCaps: true, + ignore: [] + } + ], + 'react/jsx-uses-react': RULES.OFF, + 'react/jsx-uses-vars': RULES.WARNING, + 'react/no-deprecated': RULES.WARNING, + 'react/no-did-update-set-state': RULES.ERROR, + 'react/no-direct-mutation-state': RULES.ERROR, + 'react/no-is-mounted': RULES.WARNING, + 'react/no-multi-comp': [RULES.WARNING, {ignoreStateless: true}], + 'react/no-unused-prop-types': RULES.WARNING, + 'react/no-unknown-property': RULES.ERROR, + 'react/prop-types': RULES.ERROR, + 'react/react-in-jsx-scope': RULES.OFF, + 'react/require-render-return': RULES.WARNING, + 'react/no-unstable-nested-components': RULES.WARNING +} + +const TESTING_RULES = { + 'chai-friendly/no-unused-expressions': [RULES.ERROR, {allowShortCircuit: true, allowTernary: true}], + 'no-only-tests/no-only-tests': RULES.ERROR +} + +const JEST_TESTING_RULES = { + 'react/display-name': RULES.OFF, + 'jest/consistent-test-it': RULES.OFF, + 'jest/expect-expect': RULES.OFF, + 'jest/max-expects': RULES.OFF, + 'jest/max-nested-describe': RULES.ERROR, + 'jest/no-alias-methods': RULES.OFF, + 'jest/no-commented-out-tests': RULES.WARNING, + 'jest/no-conditional-expect': RULES.ERROR, + 'jest/no-conditional-in-test': RULES.ERROR, + 'jest/no-deprecated-functions': RULES.ERROR, + 'jest/no-disabled-tests': RULES.WARNING, + 'jest/no-done-callback': RULES.ERROR, + 'jest/no-duplicate-hooks': RULES.OFF, + 'jest/no-export': RULES.ERROR, + 'jest/no-focused-tests': RULES.ERROR, + 'jest/no-hooks': RULES.OFF, + 'jest/no-identical-title': RULES.ERROR, + 'jest/no-if': RULES.ERROR, + 'jest/no-interpolation-in-snapshots': RULES.ERROR, + 'jest/no-jasmine-globals': RULES.OFF, + 'jest/no-large-snapshots': [RULES.WARNING, {maxSize: 300}], + 'jest/no-mocks-import': RULES.ERROR, + 'jest/no-restricted-matchers': RULES.OFF, + 'jest/no-standalone-expect': RULES.OFF, + 'jest/no-test-prefixes': RULES.ERROR, + 'jest/no-test-return-statement': RULES.OFF, + 'jest/prefer-called-with': RULES.ERROR, + 'jest/prefer-comparison-matcher': RULES.ERROR, + 'jest/prefer-each': RULES.ERROR, + 'jest/prefer-equality-matcher': RULES.ERROR, + 'jest/prefer-expect-assertions': RULES.OFF, + 'jest/prefer-expect-resolves': RULES.OFF, + 'jest/prefer-hooks-in-order': RULES.ERROR, + 'jest/prefer-hooks-on-top': RULES.ERROR, + 'jest/prefer-lowercase-title': RULES.OFF, + 'jest/prefer-mock-promise-shorthand': RULES.ERROR, + 'jest/prefer-snapshot-hint': RULES.ERROR, + 'jest/prefer-spy-on': RULES.OFF, + 'jest/prefer-strict-equal': RULES.OFF, + 'jest/prefer-to-be': RULES.OFF, + 'jest/prefer-to-contain': RULES.WARNING, + 'jest/prefer-to-have-length': RULES.WARNING, + 'jest/prefer-todo': RULES.WARNING, + 'jest/require-hook': RULES.OFF, + 'jest/require-to-throw-message': RULES.OFF, + 'jest/require-top-level-describe': RULES.OFF, + 'jest/unbound-method': RULES.OFF, + 'jest/valid-describe-callback': RULES.ERROR, + 'jest/valid-expect': RULES.ERROR, + 'jest/valid-expect-in-promise': RULES.ERROR, + 'jest/valid-title': RULES.WARNING, + 'jest-dom/prefer-checked': RULES.ERROR, + 'jest-dom/prefer-empty': RULES.ERROR, + 'jest-dom/prefer-enabled-disabled': RULES.ERROR, + 'jest-dom/prefer-focus': RULES.ERROR, + 'jest-dom/prefer-in-document': RULES.ERROR, + 'jest-dom/prefer-required': RULES.ERROR, + 'jest-dom/prefer-to-have-attribute': RULES.ERROR, + 'jest-dom/prefer-to-have-class': RULES.ERROR, + 'jest-dom/prefer-to-have-style': RULES.ERROR, + 'jest-dom/prefer-to-have-text-content': RULES.ERROR, + 'jest-dom/prefer-to-have-value': RULES.ERROR +} + +const IMPORT_SORT_GROUPS = [ + // Side effect and polyfill imports. + ['^\\u0000'], + // Built-in node dependencies + [ + '^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)' + ], + // Packages. `react` related packages come first. + ['^react'], + // Standalone packages. + ['^\\w'], + // Generic organization packages. + ['^@'], + // S-UI & ADV-UI organization packages. + ['^@s-ui', '^@adv-ui'], + // Relative imports. Put `./` last. + ['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], + // Style imports. + ['^.+\\.s?css$'] +] + +let resolvedBabelPresetSui +try { + resolvedBabelPresetSui = require.resolve('babel-preset-sui') +} catch {} + +const parser = resolvedBabelPresetSui ? '@babel/eslint-parser' : undefined + +module.exports = { + parser, + + env: { + es6: true, + mocha: true, + 'jest/globals': true + }, + + globals: { + 'cypress/globals': true, + preval: 'readonly' + }, + + parserOptions: { + ecmaFeatures: { + jsx: true + }, + ecmaVersion: 12, + babelOptions: { + configFile: resolvedBabelPresetSui + } + }, + + settings: { + react: { + version: 'detect' + } + }, + + extends: ['standard', 'plugin:cypress/recommended', 'prettier'], + + plugins: [ + '@babel', + 'chai-friendly', + 'no-only-tests', + 'prettier', + 'react', + 'react-hooks', + 'simple-import-sort', + 'jest', + 'jest-dom' + // 'sui' + ], + rules: { + ...REACT_RULES, + ...TESTING_RULES, + '@babel/no-unused-expressions': RULES.OFF, + 'accessor-pairs': RULES.OFF, + 'array-callback-return': RULES.WARNING, + 'import/no-webpack-loader-syntax': RULES.WARNING, + 'import/extensions': [RULES.WARNING, 'always', {ignorePackages: true}], + 'n/no-path-concat': RULES.WARNING, + 'no-console': RULES.WARNING, + 'no-debugger': RULES.ERROR, + 'no-nested-ternary': RULES.WARNING, + 'no-prototype-builtins': RULES.OFF, + 'no-return-await': RULES.WARNING, + 'no-unused-expressions': RULES.OFF, + 'no-unused-vars': [RULES.ERROR, {args: 'none', ignoreRestSiblings: true, varsIgnorePattern: 'React'}], + 'no-var': RULES.WARNING, + strict: RULES.OFF, + 'prefer-regex-literals': RULES.WARNING, + 'prettier/prettier': [RULES.ERROR, prettierOptions], + 'simple-import-sort/imports': [RULES.WARNING, {groups: IMPORT_SORT_GROUPS}], + 'react/jsx-no-bind': RULES.OFF + // 'sui/factory-pattern': RULES.WARNING, + // 'sui/serialize-deserialize': RULES.WARNING + }, + overrides: [ + { + files: ['**/*.+(ts|tsx)'], + extends: ['standard-with-typescript', 'standard-react', 'prettier'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json' + }, + rules: { + 'import/extensions': RULES.OFF, + 'no-return-await': RULES.OFF, + 'prettier/prettier': [RULES.ERROR, prettierOptions], + 'react/react-in-jsx-scope': RULES.OFF, + 'react/no-unused-prop-types': RULES.OFF, + '@typescript-eslint/explicit-function-return-type': [RULES.OFF, {allowTypedFunctionExpressions: false}], + 'chai-friendly/no-unused-expressions': RULES.ERROR, + '@typescript-eslint/no-unused-expressions': RULES.OFF, + '@typescript-eslint/return-await': RULES.OFF + } + }, + { + files: ['**/__tests__/**/*.js'], + rules: JEST_TESTING_RULES + } + ] +} diff --git a/packages/sui-lint/package.json b/packages/sui-lint/package.json index dfb8672e4..fa6ab1976 100644 --- a/packages/sui-lint/package.json +++ b/packages/sui-lint/package.json @@ -20,9 +20,11 @@ "@s-ui/helpers": "1", "@typescript-eslint/eslint-plugin": "5.62.0", "commander": "8.3.0", - "eslint": "8.20.0", + "console-table-printer": "2.12.0", + "eslint": "8.56.0", "eslint-config-prettier": "8.5.0", "eslint-config-standard": "17.0.0", + "eslint-config-standard-react": "^13.0.0", "eslint-config-standard-with-typescript": "22.0.0", "eslint-plugin-chai-friendly": "0.7.2", "eslint-plugin-cypress": "2.12.1", @@ -36,6 +38,10 @@ "eslint-plugin-react": "7.30.1", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-simple-import-sort": "7.0.0", + "eslint-plugin-sui": "beta", + "fast-glob": "3.3.2", + "js-yaml": "4.1.0", + "lint-repository-sui": "beta", "postcss-scss": "4.0.4", "prettier": "2.7.1", "stylelint": "14.11.0", diff --git a/packages/sui-lint/repository.config.js b/packages/sui-lint/repository.config.js new file mode 100644 index 000000000..bd8ef0308 --- /dev/null +++ b/packages/sui-lint/repository.config.js @@ -0,0 +1,16 @@ +const RULES = { + OFF: 0, + WARNING: 1, + ERROR: 2 +} + +module.exports = { + plugins: ['sui'], + rules: { + 'sui/node-version': RULES.WARNING, + 'sui/react-version': RULES.WARNING, + 'sui/package-lock': RULES.WARNING, + 'sui/github-action': RULES.WARNING, + 'sui/typescript': RULES.WARNING + } +} diff --git a/packages/sui-lint/src/RepositoryLinter/Config.js b/packages/sui-lint/src/RepositoryLinter/Config.js new file mode 100644 index 000000000..57d09eaf5 --- /dev/null +++ b/packages/sui-lint/src/RepositoryLinter/Config.js @@ -0,0 +1,31 @@ +module.exports.Config = class Config { + static create() { + return new Config() + } + + async load() { + const repositoryConfig = this.requireConfig() + + const rules = repositoryConfig.plugins.reduce((acc, pkg) => { + const pkgConfig = this.requirePkg(pkg) + const rulesEntries = Object.entries(pkgConfig.rules) + .map(([rule, handler]) => { + const key = `${pkg}/${rule}` + const level = repositoryConfig.rules[key] + return level ? [key, {handler, level}] : [] + }) + .filter(([key, value]) => key && value) + return {...acc, ...Object.fromEntries(rulesEntries)} + }, {}) + + return rules + } + + requireConfig() { + return require('../../repository.config.js') + } + + requirePkg(pkg) { + return require(`lint-repository-${pkg}`) + } +} diff --git a/packages/sui-lint/src/RepositoryLinter/Context.js b/packages/sui-lint/src/RepositoryLinter/Context.js new file mode 100644 index 000000000..e04f769f6 --- /dev/null +++ b/packages/sui-lint/src/RepositoryLinter/Context.js @@ -0,0 +1,69 @@ +const {Runner} = require('./Runner') + +const EMPTY = 0 + +module.exports.Context = class Context { + #messages = [] + #monitorings = [] + #handler + #runner + + static MISSING_REDUCER_MONITORING_MSG = ` + [RepositoryLinter Context#signal] If your has call to 'context.monitoring' more than one time in your rule. + You have to create a function 'reduceMonitoring' to be able reduce all of them to 1 value. + ` + + static create(level, handler, rule, runner) { + return new Context(level, handler, rule, runner ?? Runner.create()) + } + + constructor(level, handler, rule, runner) { + this.#handler = handler + this.#runner = runner + this.rule = rule + this.level = level + } + + run() { + const assertions = this.#handler.create(this) + const {missmatch = () => {}, ...restAssertions} = assertions + Object.entries(restAssertions).forEach(([key, fn]) => { + const matches = this.#runner.assertion(key) + if (matches.length === EMPTY) { + this._assertion = 'missmatch' + return missmatch(key) + } + this._assertion = key // We cant execute assertions in parallel + fn(matches) + }) + return this + } + + get messages() { + return this.#messages.map(opts => { + let message = this.#handler?.meta?.messages[opts.messageId] ?? opts.messageId + message = Object.entries(opts.data ?? {}).reduce((acc, [key, value]) => { + return acc.replaceAll(`{{${key}}}`, value) + }, message) + return {...opts, message, rule: this.rule, level: this.level} + }) + } + + get signal() { + const _signal = {rule: this.rule, level: this.level} + if (this.#monitorings.length === 0) return _signal + if (this.#monitorings.length === 1) return {..._signal, value: this.#monitorings[0].value} + + if (this.#handler.reduceMonitoring === undefined) throw new Error(Context.MISSING_REDUCER_MONITORING_MSG) + + return {rule: this.rule, level: this.level, value: this.#handler?.reduceMonitoring(this.#monitorings)} + } + + report(opts) { + this.#messages.push(opts) + } + + monitoring(value, assertion) { + this.#monitorings.push({assertion: assertion ?? this._assertion, rule: this.rule, value, level: this.level}) + } +} diff --git a/packages/sui-lint/src/RepositoryLinter/Match.js b/packages/sui-lint/src/RepositoryLinter/Match.js new file mode 100644 index 000000000..ffb826b85 --- /dev/null +++ b/packages/sui-lint/src/RepositoryLinter/Match.js @@ -0,0 +1,52 @@ +const {extname} = require('path') +const yaml = require('js-yaml') +const fs = require('fs') + +class CustomFileReader { + static create() { return new CustomFileReader() } // eslint-disable-line + + isDirectory(path) { return fs.statSync(process.cwd() + '/' + path).isDirectory() } // eslint-disable-line + parseYML(path) { return yaml.load(fs.readFileSync(process.cwd() + '/' + path, 'utf8')) } // eslint-disable-line + parseJSON(path) { return require(process.cwd() + '/' + path) } // eslint-disable-line + raw(path) { return fs.readFileSync(process.cwd() + '/' + path, 'utf8') } // eslint-disable-line +} + +class Match { + static empty() { + return new Match(undefined, undefined, undefined, false) + } + + static create(path) { + const ext = extname(path) + if (!ext && CustomFileReader.create().isDirectory(path)) { + return new Match(path, undefined, undefined, true) + } + + let parsed + let raw + switch (ext) { + case '.json': + parsed = CustomFileReader.create().parseJSON(path) + break + case '.yml': + case '.yaml': + parsed = CustomFileReader.create().parseYML(path) + break + default: + raw = CustomFileReader.create().raw(path) + } + + return new Match(path, parsed, raw, false) + } + + constructor(path, parsed, raw, isDir) { + this.parsed = parsed + this.raw = raw + this.path = path + this.isDir = isDir + this.fullPath = process.cwd() + '/' + path + } +} + +module.exports.CustomFileReader = CustomFileReader +module.exports.Match = Match diff --git a/packages/sui-lint/src/RepositoryLinter/Results.js b/packages/sui-lint/src/RepositoryLinter/Results.js new file mode 100644 index 000000000..00de9d780 --- /dev/null +++ b/packages/sui-lint/src/RepositoryLinter/Results.js @@ -0,0 +1,63 @@ +const dedent = require('string-dedent') +const {Table} = require('console-table-printer') + +const COLORS_BY_LEVEL = ['green', 'yellow', 'red'] + +module.exports.Results = class Results { + #executions + #messages + #monitorings + + static HAPPY_MESSAGE = dedent` + 🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳 + 🥳 Your repository follow all our internal conventions 🥳 + 🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳 + ` + + static create(executions) { + return new Results(executions) + } + + constructor(executions) { + this.#executions = executions + this.#messages = executions.reduce((acc, ctxt) => [...acc, ...ctxt.messages], []) + this.#monitorings = executions.map(ctxt => ctxt.signal) + } + + get monitorings() { + return this.#monitorings + } + + log(msg) { console.log(msg) } // eslint-disable-line + + logTable() { + if (this.#messages.length === 0) return this.log(Results.HAPPY_MESSAGE) + + const p = new Table({ + title: dedent` + Lint Respository messages (green=OFF, yellow=WARNING, red=ERROR) + ` + }) + this.#messages.forEach(msg => + p.addRow({rule: msg.rule, message: msg.message.replaceAll('\n', ' ')}, {color: COLORS_BY_LEVEL[msg.level]}) + ) + p.printTable() + } + + logJSON() { + return this.log(JSON.stringify(this.#messages, null, 2)) + } + + logMonitorings() { + if (this.#monitorings.length === 0) + return this.log(dedent` + + There is not signal to be send. Use 'context.monitoring' to add signals at your execution + + `) + + const p = new Table({title: 'List of Signals that will be send to DD'}) + this.#monitorings.forEach(monitor => p.addRow(monitor)) + p.printTable() + } +} diff --git a/packages/sui-lint/src/RepositoryLinter/Runner.js b/packages/sui-lint/src/RepositoryLinter/Runner.js new file mode 100644 index 000000000..8c3ffc81f --- /dev/null +++ b/packages/sui-lint/src/RepositoryLinter/Runner.js @@ -0,0 +1,17 @@ +const {Match} = require('./Match') +const fastGlob = require('fast-glob') + +module.exports.Runner = class Runner { + static create(fg) { + return new Runner(fg ?? fastGlob) + } + + constructor(fg) { + this.fg = fg + } + + assertion(key) { + const files = this.fg.sync(key, {ignore: ['node_modules'], onlyFiles: false}) ?? [] + return files.map(Match.create) + } +} diff --git a/packages/sui-lint/src/RepositoryLinter/index.js b/packages/sui-lint/src/RepositoryLinter/index.js new file mode 100644 index 000000000..0178a72d3 --- /dev/null +++ b/packages/sui-lint/src/RepositoryLinter/index.js @@ -0,0 +1,24 @@ +const {Config} = require('./Config') +const {Context} = require('./Context') +const {Results} = require('./Results') + +module.exports.RepositoryLinter = class RepositoryLinter { + #cofig + + static create(config) { + return new RepositoryLinter(config ?? Config.create()) + } + + constructor(config) { + this.#cofig = config + } + + async lint() { + const rules = await this.#cofig.load() + const executions = Object.entries(rules).map(([rule, {handler, level}]) => + Context.create(level, handler, rule).run() + ) + + return Results.create(executions) + } +} diff --git a/packages/sui-lint/test/server/ConfigSpec.js b/packages/sui-lint/test/server/ConfigSpec.js new file mode 100644 index 000000000..0261735b2 --- /dev/null +++ b/packages/sui-lint/test/server/ConfigSpec.js @@ -0,0 +1,38 @@ +import {expect} from 'chai' +import {stub} from 'sinon' + +import {Config} from '../../../src/RepositoryLinter/Config.js' + +describe('Config', function () { + beforeEach(function () { + this.requireConfigStub = stub(Config.prototype, 'requireConfig') + this.requirePkgStub = stub(Config.prototype, 'requirePkg') + }) + + afterEach(function () { + this.requireConfigStub.reset() + this.requirePkgStub.reset() + }) + + it('Should return a rules object', async function () { + // Given + const handler = { + meta: {messages: {badVersion: 'Your react version is not 20'}}, + create: function () {} + } + this.requireConfigStub.returns({ + plugins: ['tester'], + rules: { + 'tester/react-version': 1 + } + }) + this.requirePkgStub.returns({rules: {'react-version': handler}}) + + // When + const rulesLoaded = await Config.create().load() + + // Then + expect(this.requirePkgStub.calledWith('tester')).to.be.eql(true) + expect(rulesLoaded).to.be.eqls({'tester/react-version': {handler, level: 1}}) + }) +}) diff --git a/packages/sui-lint/test/server/ContextSpec.js b/packages/sui-lint/test/server/ContextSpec.js new file mode 100644 index 000000000..fd640bcd4 --- /dev/null +++ b/packages/sui-lint/test/server/ContextSpec.js @@ -0,0 +1,206 @@ +import {expect} from 'chai' +import {spy, stub} from 'sinon' + +import {Context} from '../../src/RepositoryLinter/Context.js' +import {Match} from '../../src/RepositoryLinter/Match.js' + +const LEVELS = { + OFF: 0, + WARNING: 1, + ERROR: 2 +} + +describe('Context', function () { + beforeEach(function () { + this.reportSpy = spy(Context.prototype, 'report') + this.monitoringSpy = spy(Context.prototype, 'monitoring') + this.runnerStub = {assertion: stub()} + this.handlerStub = { + meta: {messages: {badVersion: 'Message for your bad version'}}, + __assertionStub: stub(), + __missmatchStub: stub(), + create() { + return { + 'package.json': this.__assertionStub, + missmatch: this.__missmatchStub + } + } + } + + this.handlerInnerStub = { + meta: {messages: {badVersion: 'Message for your bad version', badKey: 'Message for bad {{key}}'}}, + create(context) { + return { + 'package.json': matches => { + context.report({messageId: 'badVersion'}) + context.monitoring(true) + }, + missmatch: key => { + context.report({messageId: 'badKey', data: {key}}) + context.monitoring(false) + } + } + } + } + + this.handlerReducerMonitoringStub = { + meta: {messages: {badVersion: 'Message for your bad version', badKey: 'Message for bad {{key}}'}}, + reduceMonitoring: stub(), + create(context) { + return { + 'package.json': matches => { + context.monitoring(true) + context.monitoring(false) + } + } + } + } + }) + + afterEach(function () { + this.handlerStub.__assertionStub.reset() + this.handlerStub.__missmatchStub.reset() + + this.monitoringSpy.restore() + this.reportSpy.restore() + + this.runnerStub.assertion.reset() + + this.handlerReducerMonitoringStub.reduceMonitoring?.reset() + }) + + it('Should call to the assertions in the handler when there is Match', function () { + // Given + const emptyMatch = Match.empty() + this.runnerStub.assertion.returns([emptyMatch]) + + // When + Context.create(LEVELS.WARNING, this.handlerStub, 'tests/node-version', this.runnerStub).run() + + // Then + expect(this.handlerStub.__assertionStub.firstCall.firstArg).to.be.instanceof(Array) + expect(this.handlerStub.__assertionStub.firstCall.firstArg[0]).to.be.eql(emptyMatch) + }) + + it('Should call to the missmatch in the handler when there is not Match with the "failing" key', function () { + // Given + this.runnerStub.assertion.returns([]) + + // When + Context.create(LEVELS.WARNING, this.handlerStub, 'tests/node-version', this.runnerStub).run() + + // Then + expect(this.handlerStub.__assertionStub.firstCall).to.be.eql(null) + expect(this.handlerStub.__missmatchStub.firstCall.firstArg).to.be.eql('package.json') + }) + + it('Should create new monitorings and messages from assertion function', function () { + // Given + this.runnerStub.assertion.returns([Match.empty()]) + + // When + Context.create(LEVELS.WARNING, this.handlerInnerStub, 'tests/node-version', this.runnerStub).run() + + // Then + expect(this.reportSpy.firstCall.firstArg).to.be.eql({messageId: 'badVersion'}) + expect(this.monitoringSpy.firstCall.firstArg).to.be.eql(true) + }) + + it('Should create new monitorings and messages from missmatch function', function () { + // Given + this.runnerStub.assertion.returns([]) + + // When + Context.create(LEVELS.WARNING, this.handlerInnerStub, 'tests/node-version', this.runnerStub).run() + + // Then + expect(this.reportSpy.firstCall.firstArg).to.be.eql({messageId: 'badKey', data: {key: 'package.json'}}) + expect(this.monitoringSpy.firstCall.firstArg).to.be.eql(false) + }) + + it('Should properly format the messages', function () { + // Given + this.runnerStub.assertion.returns([]) + + // When + const context = Context.create(LEVELS.WARNING, this.handlerInnerStub, 'tests/node-version', this.runnerStub).run() + + // Then + expect(context.messages).to.be.eql([ + { + rule: 'tests/node-version', + message: 'Message for bad package.json', + level: 1, + messageId: 'badKey', + data: {key: 'package.json'} + } + ]) + }) + + it('Should properly format the monitoring', function () { + // Given + this.runnerStub.assertion.returns([Match.empty()]) + + // When + const context = Context.create(LEVELS.WARNING, this.handlerInnerStub, 'tests/node-version', this.runnerStub).run() + + // Then + expect(context.signal).to.be.eql({ + rule: 'tests/node-version', + value: true, + level: 1 + }) + }) + + it('Should require a reduceMonitoring function when there are more than one monitor', function () { + // Given + this.runnerStub.assertion.returns([Match.empty()]) + this.handlerReducerMonitoringStub.reduceMonitoring.returns(false) + + // When + const context = Context.create( + LEVELS.WARNING, + this.handlerReducerMonitoringStub, + 'tests/node-version', + this.runnerStub + ).run() + + // Then + expect(context.signal).to.be.eql({ + rule: 'tests/node-version', + value: false, + level: 1 + }) + expect(this.handlerReducerMonitoringStub.reduceMonitoring.firstCall.firstArg).to.be.eql([ + { + assertion: 'package.json', + rule: 'tests/node-version', + value: true, + level: 1 + }, + { + assertion: 'package.json', + rule: 'tests/node-version', + value: false, + level: 1 + } + ]) + }) + + it('Should throw an exception if the reduceMonitoring function is undefined', function () { + // Given + this.runnerStub.assertion.returns([Match.empty()]) + delete this.handlerReducerMonitoringStub.reduceMonitoring + + // When + const context = Context.create( + LEVELS.WARNING, + this.handlerReducerMonitoringStub, + 'tests/node-version', + this.runnerStub + ).run() + + // Then + expect(() => context.signal).to.be.throw(Context.MISSING_REDUCER_MONITORING_MSG) + }) +}) diff --git a/packages/sui-lint/test/server/MatchSpec.js b/packages/sui-lint/test/server/MatchSpec.js new file mode 100644 index 000000000..5db0c60c7 --- /dev/null +++ b/packages/sui-lint/test/server/MatchSpec.js @@ -0,0 +1,99 @@ +import {expect} from 'chai' +import {stub} from 'sinon' + +import {CustomFileReader, Match} from '../../src/RepositoryLinter/Match.js' + +describe('Match', function () { + beforeEach(function () { + this.isDirStub = stub(CustomFileReader.prototype, 'isDirectory') + + this.parseYMLStub = stub(CustomFileReader.prototype, 'parseYML') + this.parseJSONStub = stub(CustomFileReader.prototype, 'parseJSON') + this.rawStub = stub(CustomFileReader.prototype, 'raw') + }) + + afterEach(function () { + this.isDirStub.restore() + + this.parseYMLStub.restore() + this.parseJSONStub.restore() + this.rawStub.restore() + }) + + it('Should detect directories', function () { + // Given + this.isDirStub.returns(true) + + // When + const match = Match.create('/dir/path') + + // Then + expect(match.isDir).to.be.eqls(true) + }) + + it('Should detect files w/out extensions', function () { + // Given + this.isDirStub.returns(false) + this.rawStub.returns('20') + + // When + const match = Match.create('/dir/path') + + // Then + expect(match.isDir).to.be.eqls(false) + expect(this.rawStub.calledWith('/dir/path')).to.be.eql(true) + }) + + it('Should parse JSON files', function () { + // Given + this.isDirStub.returns(false) + this.parseJSONStub.returns({a: 1}) + + // When + const match = Match.create('/dir/file.json') + + // Then + expect(match.parsed).to.be.eqls({a: 1}) + expect(this.parseJSONStub.calledWith('/dir/file.json')).to.be.eql(true) + }) + + it('Should parse yml files', function () { + // Given + this.isDirStub.returns(false) + this.parseYMLStub.returns({a: 1}) + + // When + const match = Match.create('/dir/file.yml') + + // Then + expect(match.parsed).to.be.eqls({a: 1}) + expect(this.parseYMLStub.calledWith('/dir/file.yml')).to.be.eql(true) + }) + + it('Should parse yaml files', function () { + // Given + this.isDirStub.returns(false) + this.parseYMLStub.returns({a: 1}) + + // When + const match = Match.create('/dir/file.yaml') + + // Then + expect(match.parsed).to.be.eqls({a: 1}) + expect(this.parseYMLStub.calledWith('/dir/file.yaml')).to.be.eql(true) + }) + + it('Should read unkown files', function () { + // Given + this.isDirStub.returns(false) + this.rawStub.returns('Hello') + + // When + const match = Match.create('/dir/file.txt') + + // Then + expect(match.raw).to.be.eqls('Hello') + expect(match.parsed).to.be.eqls(undefined) + expect(this.rawStub.calledWith('/dir/file.txt')).to.be.eql(true) + }) +}) diff --git a/packages/sui-lint/test/server/ResultsSpec.js b/packages/sui-lint/test/server/ResultsSpec.js new file mode 100644 index 000000000..ad9cc62c5 --- /dev/null +++ b/packages/sui-lint/test/server/ResultsSpec.js @@ -0,0 +1,83 @@ +import {expect} from 'chai' +import {Table} from 'console-table-printer' +import {stub} from 'sinon' + +import {Results} from '../../src/RepositoryLinter/Results.js' + +describe('Results', function () { + beforeEach(function () { + this.addRowStub = stub(Table.prototype, 'addRow') + this.printTableStub = stub(Table.prototype, 'printTable') + + this.logStub = stub(Results.prototype, 'log') + }) + + afterEach(function () { + this.addRowStub.restore() + this.printTableStub.restore() + + this.logStub.restore() + }) + + it('Should print Happy Message it there is not messages', function () { + const executions = [ + { + messages: [], + signal: false + } + ] + Results.create(executions).logTable() + + expect(this.logStub.calledWith(Results.HAPPY_MESSAGE)).to.be.eq(true) + }) + + it('Should print a table with all the messages', function () { + const executions = [ + {messages: [{rule: 'tester/node-version', message: 'Node version fail', level: 1}], signal: 12}, + {messages: [{rule: 'tester/react-version', message: 'React version fail', level: 2}], signal: 17}, + {messages: [], signal: true} + ] + Results.create(executions).logTable() + + expect(this.addRowStub.firstCall.firstArg).to.be.eql({rule: 'tester/node-version', message: 'Node version fail'}) + expect(this.addRowStub.firstCall.lastArg).to.be.eql({color: 'yellow'}) + + expect(this.addRowStub.secondCall.firstArg).to.be.eql({rule: 'tester/react-version', message: 'React version fail'}) + expect(this.addRowStub.secondCall.lastArg).to.be.eql({color: 'red'}) + }) + + it('Should print a table with all the monitorings', function () { + const executions = [ + {messages: [{rule: 'tester/node-version', message: 'Node version fail', level: 1}], signal: 12}, + {messages: [{rule: 'tester/react-version', message: 'React version fail', level: 2}], signal: 17}, + {messages: [], signal: true} + ] + Results.create(executions).logMonitorings() + + expect(this.addRowStub.firstCall.firstArg).to.be.eql(12) + expect(this.addRowStub.secondCall.firstArg).to.be.eql(17) + expect(this.addRowStub.thirdCall.firstArg).to.be.eql(true) + }) + + it('Should print a JSON output', function () { + const executions = [ + {messages: [{rule: 'tester/node-version', message: 'Node version fail', level: 1}], signal: 12}, + {messages: [{rule: 'tester/react-version', message: 'React version fail', level: 2}], signal: 17}, + {messages: [], signal: true} + ] + Results.create(executions).logJSON() + + expect( + this.logStub.calledWith( + JSON.stringify( + [ + {rule: 'tester/node-version', message: 'Node version fail', level: 1}, + {rule: 'tester/react-version', message: 'React version fail', level: 2} + ], + null, + 2 + ) + ) + ).to.be.eq(true) + }) +}) diff --git a/packages/sui-lint/test/server/RunnerSpec.js b/packages/sui-lint/test/server/RunnerSpec.js new file mode 100644 index 000000000..282c75406 --- /dev/null +++ b/packages/sui-lint/test/server/RunnerSpec.js @@ -0,0 +1,26 @@ +import {expect} from 'chai' +import {stub} from 'sinon' + +import {Match} from '../../src/RepositoryLinter/Match.js' +import {Runner} from '../../src/RepositoryLinter/Runner.js' + +describe('Runner', function () { + beforeEach(function () { + this.syncStub = stub() + this.matchCreateStub = stub(Match, 'create') + }) + + afterEach(function () { + this.syncStub.reset() + this.matchCreateStub.restore() + }) + + it('Should return a list of matches', function () { + this.syncStub.returns(['path/file.json']) + + Runner.create({sync: this.syncStub}).assertion('**/*.json') + + expect(this.matchCreateStub.firstCall.firstArg).to.be.eql('path/file.json') + expect(this.syncStub.firstCall.firstArg).to.be.eql('**/*.json') + }) +})