From 632ae0b7c3c4beb64bb518398a706cc6036ce764 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 24 Apr 2023 15:44:42 +0100 Subject: [PATCH] Prevent new JS files in shared folder (#17737) * Prevent new JS files in shared folder * migrate to typescript * fix types * cleanup --- .github/workflows/fitness-functions.yml | 2 +- .../fitness-functions/check-mocha-syntax.js | 44 ------- .../common/constants.test.ts | 105 +++++++++++++++ .../fitness-functions/common/constants.ts | 15 +++ .../fitness-functions/common/get-diff.ts | 54 ++++++++ .../{shared.test.js => common/shared.test.ts} | 120 ++++++------------ .../{shared.js => common/shared.ts} | 42 ++++-- .../fitness-functions/common/test-data.ts | 40 ++++++ development/fitness-functions/index.js | 53 -------- development/fitness-functions/index.ts | 11 ++ development/fitness-functions/rules/index.ts | 42 ++++++ .../rules/javascript-additions.test.ts | 51 ++++++++ .../rules/javascript-additions.ts | 18 +++ .../rules/sinon-assert-syntax.test.ts | 29 +++++ .../rules/sinon-assert-syntax.ts | 30 +++++ package.json | 2 +- 16 files changed, 467 insertions(+), 191 deletions(-) delete mode 100644 development/fitness-functions/check-mocha-syntax.js create mode 100644 development/fitness-functions/common/constants.test.ts create mode 100644 development/fitness-functions/common/constants.ts create mode 100644 development/fitness-functions/common/get-diff.ts rename development/fitness-functions/{shared.test.js => common/shared.test.ts} (59%) rename development/fitness-functions/{shared.js => common/shared.ts} (60%) create mode 100644 development/fitness-functions/common/test-data.ts delete mode 100644 development/fitness-functions/index.js create mode 100644 development/fitness-functions/index.ts create mode 100644 development/fitness-functions/rules/index.ts create mode 100644 development/fitness-functions/rules/javascript-additions.test.ts create mode 100644 development/fitness-functions/rules/javascript-additions.ts create mode 100644 development/fitness-functions/rules/sinon-assert-syntax.test.ts create mode 100644 development/fitness-functions/rules/sinon-assert-syntax.ts diff --git a/.github/workflows/fitness-functions.yml b/.github/workflows/fitness-functions.yml index 7549be6b4..1b55fb461 100644 --- a/.github/workflows/fitness-functions.yml +++ b/.github/workflows/fitness-functions.yml @@ -24,5 +24,5 @@ jobs: run: | # git fetch origin $HEAD_REF # git fetch origin $BASE_REF - # git diff origin/$BASE_REF origin/$HEAD_REF -- . ':(exclude)development/fitness-functions/*' > diff + # git diff origin/$BASE_REF origin/$HEAD_REF -- . > diff # npm run fitness-functions -- "ci" "./diff" diff --git a/development/fitness-functions/check-mocha-syntax.js b/development/fitness-functions/check-mocha-syntax.js deleted file mode 100644 index 2e4cea384..000000000 --- a/development/fitness-functions/check-mocha-syntax.js +++ /dev/null @@ -1,44 +0,0 @@ -const { - EXCLUDE_E2E_TESTS_REGEX, - filterDiffAdditions, - filterDiffByFilePath, - hasNumberOfCodeBlocksIncreased, -} = require('./shared'); - -function checkMochaSyntax(diff) { - const ruleHeading = 'favor-jest-instead-of-mocha'; - const codeBlocks = [ - "import { strict as assert } from 'assert';", - 'assert.deepEqual', - 'assert.equal', - 'assert.rejects', - 'assert.strictEqual', - 'sinon.', - ]; - - console.log(`\nChecking ${ruleHeading}...`); - - const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX); - const diffAdditions = filterDiffAdditions(diffByFilePath); - const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks); - - Object.keys(hashmap).forEach((key) => { - if (hashmap[key]) { - console.error(`Number of occurences of "${key}" have increased.`); - } - }); - - if (Object.values(hashmap).includes(true)) { - console.error( - `...changes have not been accepted by the fitness function.\nFor more info, see: https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#${ruleHeading}`, - ); - process.exit(1); - } else { - console.log( - `...number of occurences has not increased for any code block.`, - ); - process.exit(0); - } -} - -module.exports = { checkMochaSyntax }; diff --git a/development/fitness-functions/common/constants.test.ts b/development/fitness-functions/common/constants.test.ts new file mode 100644 index 000000000..21912d5fa --- /dev/null +++ b/development/fitness-functions/common/constants.test.ts @@ -0,0 +1,105 @@ +import { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX } from './constants'; + +describe('Regular Expressions used in Fitness Functions', (): void => { + describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, (): void => { + const PATHS_IT_SHOULD_MATCH = [ + 'file.js', + 'path/file.js', + 'much/longer/path/file.js', + 'file.ts', + 'path/file.ts', + 'much/longer/path/file.ts', + 'file.jsx', + 'path/file.jsx', + 'much/longer/path/file.jsx', + ]; + + const PATHS_IT_SHOULD_NOT_MATCH = [ + // any without JS, TS, JSX or TSX extension + 'file', + 'file.extension', + 'path/file.extension', + 'much/longer/path/file.extension', + // any in the test/e2e directory + 'test/e2e/file', + 'test/e2e/file.extension', + 'test/e2e/path/file.extension', + 'test/e2e/much/longer/path/file.extension', + 'test/e2e/file.js', + 'test/e2e/path/file.ts', + 'test/e2e/much/longer/path/file.jsx', + 'test/e2e/much/longer/path/file.tsx', + // any in the development/fitness-functions directory + 'development/fitness-functions/file', + 'development/fitness-functions/file.extension', + 'development/fitness-functions/path/file.extension', + 'development/fitness-functions/much/longer/path/file.extension', + 'development/fitness-functions/file.js', + 'development/fitness-functions/path/file.ts', + 'development/fitness-functions/much/longer/path/file.jsx', + 'development/fitness-functions/much/longer/path/file.tsx', + ]; + + describe('included paths', (): void => { + PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { + it(`should match "${path}"`, (): void => { + const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); + + expect(result).toStrictEqual(true); + }); + }); + }); + + describe('excluded paths', (): void => { + PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { + it(`should not match "${path}"`, (): void => { + const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); + + expect(result).toStrictEqual(false); + }); + }); + }); + }); + + describe(`SHARED_FOLDER_JS_REGEX "${SHARED_FOLDER_JS_REGEX}"`, (): void => { + const PATHS_IT_SHOULD_MATCH = [ + 'shared/file.js', + 'shared/path/file.js', + 'shared/much/longer/path/file.js', + 'shared/file.jsx', + 'shared/path/file.jsx', + 'shared/much/longer/path/file.jsx', + ]; + + const PATHS_IT_SHOULD_NOT_MATCH = [ + // any without JS or JSX extension + 'file', + 'file.extension', + 'path/file.extension', + 'much/longer/path/file.extension', + 'file.ts', + 'path/file.ts', + 'much/longer/path/file.tsx', + ]; + + describe('included paths', (): void => { + PATHS_IT_SHOULD_MATCH.forEach((path: string): void => { + it(`should match "${path}"`, (): void => { + const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); + + expect(result).toStrictEqual(true); + }); + }); + }); + + describe('excluded paths', (): void => { + PATHS_IT_SHOULD_NOT_MATCH.forEach((path: string): void => { + it(`should not match "${path}"`, (): void => { + const result = new RegExp(SHARED_FOLDER_JS_REGEX, 'u').test(path); + + expect(result).toStrictEqual(false); + }); + }); + }); + }); +}); diff --git a/development/fitness-functions/common/constants.ts b/development/fitness-functions/common/constants.ts new file mode 100644 index 000000000..0514c6ed9 --- /dev/null +++ b/development/fitness-functions/common/constants.ts @@ -0,0 +1,15 @@ +// include JS, TS, JSX, TSX files only excluding files in the e2e tests and +// fitness functions directories +const EXCLUDE_E2E_TESTS_REGEX = + '^(?!test/e2e)(?!development/fitness).*.(js|ts|jsx|tsx)$'; + +// include JS and JSX files in the shared directory only +const SHARED_FOLDER_JS_REGEX = '^(shared).*.(js|jsx)$'; + +enum AUTOMATION_TYPE { + CI = 'ci', + PRE_COMMIT_HOOK = 'pre-commit-hook', + PRE_PUSH_HOOK = 'pre-push-hook', +} + +export { EXCLUDE_E2E_TESTS_REGEX, SHARED_FOLDER_JS_REGEX, AUTOMATION_TYPE }; diff --git a/development/fitness-functions/common/get-diff.ts b/development/fitness-functions/common/get-diff.ts new file mode 100644 index 000000000..620d53b63 --- /dev/null +++ b/development/fitness-functions/common/get-diff.ts @@ -0,0 +1,54 @@ +import { execSync } from 'child_process'; +import fs from 'fs'; +import { AUTOMATION_TYPE } from './constants'; + +function getDiffByAutomationType( + automationType: AUTOMATION_TYPE, +): string | undefined { + if (!Object.values(AUTOMATION_TYPE).includes(automationType)) { + console.error('Invalid automation type.'); + process.exit(1); + } + + let diff; + if (automationType === AUTOMATION_TYPE.CI) { + const optionalArguments = process.argv.slice(3); + if (optionalArguments.length !== 1) { + console.error('Invalid number of arguments.'); + process.exit(1); + } + + diff = getCIDiff(optionalArguments[0]); + } else if (automationType === AUTOMATION_TYPE.PRE_COMMIT_HOOK) { + diff = getPreCommitHookDiff(); + } else if (automationType === AUTOMATION_TYPE.PRE_PUSH_HOOK) { + diff = getPrePushHookDiff(); + } + + return diff; +} + +function getCIDiff(path: string): string { + return fs.readFileSync(path, { + encoding: 'utf8', + flag: 'r', + }); +} + +function getPreCommitHookDiff(): string { + return execSync(`git diff --cached HEAD`).toString().trim(); +} + +function getPrePushHookDiff(): string { + const currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`) + .toString() + .trim(); + + return execSync( + `git diff ${currentBranch} origin/${currentBranch} -- . ':(exclude)development/fitness-functions/'`, + ) + .toString() + .trim(); +} + +export { getDiffByAutomationType }; diff --git a/development/fitness-functions/shared.test.js b/development/fitness-functions/common/shared.test.ts similarity index 59% rename from development/fitness-functions/shared.test.js rename to development/fitness-functions/common/shared.test.ts index 6b464aafd..92306b9d4 100644 --- a/development/fitness-functions/shared.test.js +++ b/development/fitness-functions/common/shared.test.ts @@ -1,46 +1,48 @@ -const { - EXCLUDE_E2E_TESTS_REGEX, - filterDiffAdditions, +import { + filterDiffLineAdditions, hasNumberOfCodeBlocksIncreased, filterDiffByFilePath, -} = require('./shared'); + filterDiffFileCreations, +} from './shared'; +import { generateCreateFileDiff, generateModifyFilesDiff } from './test-data'; -const generateCreateFileDiff = (filePath, content) => ` -diff --git a/${filePath} b/${filePath} -new file mode 100644 -index 000000000..30d74d258 ---- /dev/null -+++ b/${filePath} -@@ -0,0 +1 @@ -+${content} -`; - -const generateModifyFilesDiff = (filePath, addition, removal) => ` -diff --git a/${filePath} b/${filePath} -index 57d5de75c..808d8ba37 100644 ---- a/${filePath} -+++ b/${filePath} -@@ -1,3 +1,8 @@ -+${addition} -@@ -34,33 +39,4 @@ --${removal} -`; - -describe('filterDiffAdditions()', () => { - it('should return code additions in the diff', () => { +describe('filterDiffLineAdditions()', (): void => { + it('should return code additions in the diff', (): void => { const testFilePath = 'new-file.js'; const testAddition = 'foo'; const testFileDiff = generateCreateFileDiff(testFilePath, testAddition); - const actualResult = filterDiffAdditions(testFileDiff); + const actualResult = filterDiffLineAdditions(testFileDiff); const expectedResult = `+${testAddition}`; expect(actualResult).toStrictEqual(expectedResult); }); }); -describe('hasNumberOfCodeBlocksIncreased()', () => { - it('should show which code blocks have increased', () => { +describe('filterDiffFileCreations()', (): void => { + it('should return code additions in the diff', (): void => { + const testFileDiff = [ + generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), + generateCreateFileDiff('old-file.js', 'ping'), + generateModifyFilesDiff('old-file.jsx', 'yin', 'yang'), + ].join(''); + + const actualResult = filterDiffFileCreations(testFileDiff); + + expect(actualResult).toMatchInlineSnapshot(` + "diff --git a/old-file.js b/old-file.js + new file mode 100644 + index 000000000..30d74d258 + --- /dev/null + +++ b/old-file.js + @@ -0,0 +1 @@ + +ping" + `); + }); +}); + +describe('hasNumberOfCodeBlocksIncreased()', (): void => { + it('should show which code blocks have increased', (): void => { const testDiffFragment = ` +foo +bar @@ -57,14 +59,14 @@ describe('hasNumberOfCodeBlocksIncreased()', () => { }); }); -describe('filterDiffByFilePath()', () => { +describe('filterDiffByFilePath()', (): void => { const testFileDiff = [ generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), generateModifyFilesDiff('old-file.js', 'ping', 'pong'), generateModifyFilesDiff('old-file.jsx', 'yin', 'yang'), ].join(''); - it('should return the right diff for a generic matcher', () => { + it('should return the right diff for a generic matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, '.*/.*.(js|ts)$|.*.(js|ts)$', @@ -90,7 +92,7 @@ describe('filterDiffByFilePath()', () => { `); }); - it('should return the right diff for a specific file in any dir matcher', () => { + it('should return the right diff for a specific file in any dir matcher', (): void => { const actualResult = filterDiffByFilePath(testFileDiff, '.*old-file.js$'); expect(actualResult).toMatchInlineSnapshot(` @@ -105,7 +107,7 @@ describe('filterDiffByFilePath()', () => { `); }); - it('should return the right diff for a multiple file extension (OR) matcher', () => { + it('should return the right diff for a multiple file extension (OR) matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, '^(./)*old-file.(js|ts|jsx)$', @@ -131,7 +133,7 @@ describe('filterDiffByFilePath()', () => { `); }); - it('should return the right diff for a file name negation matcher', () => { + it('should return the right diff for a file name negation matcher', (): void => { const actualResult = filterDiffByFilePath( testFileDiff, '^(?!.*old-file.js$).*.[a-zA-Z]+$', @@ -157,51 +159,3 @@ describe('filterDiffByFilePath()', () => { `); }); }); - -describe(`EXCLUDE_E2E_TESTS_REGEX "${EXCLUDE_E2E_TESTS_REGEX}"`, () => { - const PATHS_IT_SHOULD_MATCH = [ - 'file.js', - 'path/file.js', - 'much/longer/path/file.js', - 'file.ts', - 'path/file.ts', - 'much/longer/path/file.ts', - 'file.jsx', - 'path/file.jsx', - 'much/longer/path/file.jsx', - ]; - - const PATHS_IT_SHOULD_NOT_MATCH = [ - 'test/e2e/file', - 'test/e2e/file.extension', - 'test/e2e/path/file.extension', - 'test/e2e/much/longer/path/file.extension', - 'test/e2e/file.js', - 'test/e2e/path/file.ts', - 'test/e2e/much/longer/path/file.jsx', - 'file', - 'file.extension', - 'path/file.extension', - 'much/longer/path/file.extension', - ]; - - describe('included paths', () => { - PATHS_IT_SHOULD_MATCH.forEach((path) => { - it(`should match "${path}"`, () => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - - expect(result).toStrictEqual(true); - }); - }); - }); - - describe('excluded paths', () => { - PATHS_IT_SHOULD_NOT_MATCH.forEach((path) => { - it(`should not match "${path}"`, () => { - const result = new RegExp(EXCLUDE_E2E_TESTS_REGEX, 'u').test(path); - - expect(result).toStrictEqual(false); - }); - }); - }); -}); diff --git a/development/fitness-functions/shared.js b/development/fitness-functions/common/shared.ts similarity index 60% rename from development/fitness-functions/shared.js rename to development/fitness-functions/common/shared.ts index 791837a6d..e24c8cf01 100644 --- a/development/fitness-functions/shared.js +++ b/development/fitness-functions/common/shared.ts @@ -1,6 +1,4 @@ -const EXCLUDE_E2E_TESTS_REGEX = '^(?!test/e2e/).*.(js|ts|jsx)$'; - -function filterDiffByFilePath(diff, regex) { +function filterDiffByFilePath(diff: string, regex: string): string { // split by `diff --git` and remove the first element which is empty const diffBlocks = diff.split(`diff --git`).slice(1); @@ -34,7 +32,7 @@ function filterDiffByFilePath(diff, regex) { return filteredDiff; } -function filterDiffAdditions(diff) { +function filterDiffLineAdditions(diff: string): string { const diffLines = diff.split('\n'); const diffAdditionLines = diffLines.filter((line) => { @@ -46,10 +44,36 @@ function filterDiffAdditions(diff) { return diffAdditionLines.join('/n'); } -function hasNumberOfCodeBlocksIncreased(diffFragment, codeBlocks) { +function filterDiffFileCreations(diff: string): string { + // split by `diff --git` and remove the first element which is empty + const diffBlocks = diff.split(`diff --git`).slice(1); + + const filteredDiff = diffBlocks + .map((block) => block.trim()) + .filter((block) => { + const isFileCreationLine = + block + // get the second line of the block which has the file mode + .split('\n')[1] + .trim() + .substring(0, 13) === 'new file mode'; + + return isFileCreationLine; + }) + // prepend `git --diff` to each block + .map((block) => `diff --git ${block}`) + .join('\n'); + + return filteredDiff; +} + +function hasNumberOfCodeBlocksIncreased( + diffFragment: string, + codeBlocks: string[], +): { [codeBlock: string]: boolean } { const diffLines = diffFragment.split('\n'); - const codeBlockFound = {}; + const codeBlockFound: { [codeBlock: string]: boolean } = {}; for (const codeBlock of codeBlocks) { codeBlockFound[codeBlock] = false; @@ -65,9 +89,9 @@ function hasNumberOfCodeBlocksIncreased(diffFragment, codeBlocks) { return codeBlockFound; } -module.exports = { - EXCLUDE_E2E_TESTS_REGEX, +export { filterDiffByFilePath, - filterDiffAdditions, + filterDiffLineAdditions, + filterDiffFileCreations, hasNumberOfCodeBlocksIncreased, }; diff --git a/development/fitness-functions/common/test-data.ts b/development/fitness-functions/common/test-data.ts new file mode 100644 index 000000000..7dac4dee1 --- /dev/null +++ b/development/fitness-functions/common/test-data.ts @@ -0,0 +1,40 @@ +const generateCreateFileDiff = ( + filePath = 'file.txt', + content = 'Lorem ipsum', +): string => ` +diff --git a/${filePath} b/${filePath} +new file mode 100644 +index 000000000..30d74d258 +--- /dev/null ++++ b/${filePath} +@@ -0,0 +1 @@ ++${content} +`; + +const generateModifyFilesDiff = ( + filePath = 'file.txt', + addition = 'Lorem ipsum', + removal = '', +): string => { + const additionBlock = addition + ? ` +@@ -1,3 +1,8 @@ ++${addition}`.trim() + : ''; + + const removalBlock = removal + ? ` +@@ -34,33 +39,4 @@ +-${removal}`.trim() + : ''; + + return ` +diff --git a/${filePath} b/${filePath} +index 57d5de75c..808d8ba37 100644 +--- a/${filePath} ++++ b/${filePath} +${additionBlock} +${removalBlock}`; +}; + +export { generateCreateFileDiff, generateModifyFilesDiff }; diff --git a/development/fitness-functions/index.js b/development/fitness-functions/index.js deleted file mode 100644 index 37b61819d..000000000 --- a/development/fitness-functions/index.js +++ /dev/null @@ -1,53 +0,0 @@ -const fs = require('fs'); -const { execSync } = require('child_process'); -const { checkMochaSyntax } = require('./check-mocha-syntax'); - -const AUTOMATION_TYPE = Object.freeze({ - CI: 'ci', - PRE_COMMIT_HOOK: 'pre-commit-hook', - PRE_PUSH_HOOK: 'pre-push-hook', -}); - -const automationType = process.argv[2]; - -if (automationType === AUTOMATION_TYPE.CI) { - const optionalArguments = process.argv.slice(3); - if (optionalArguments.length !== 1) { - console.error('Invalid number of arguments.'); - process.exit(1); - } - - const diff = fs.readFileSync(optionalArguments[0], { - encoding: 'utf8', - flag: 'r', - }); - - checkMochaSyntax(diff); -} else if (automationType === AUTOMATION_TYPE.PRE_COMMIT_HOOK) { - const diff = getPreCommitHookDiff(); - - checkMochaSyntax(diff); -} else if (automationType === AUTOMATION_TYPE.PRE_PUSH_HOOK) { - const diff = getPrePushHookDiff(); - - checkMochaSyntax(diff); -} else { - console.error('Invalid automation type.'); - process.exit(1); -} - -function getPreCommitHookDiff() { - return execSync(`git diff --cached HEAD`).toString().trim(); -} - -function getPrePushHookDiff() { - const currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`) - .toString() - .trim(); - - return execSync( - `git diff ${currentBranch} origin/${currentBranch} -- . ':(exclude)development/fitness-functions/'`, - ) - .toString() - .trim(); -} diff --git a/development/fitness-functions/index.ts b/development/fitness-functions/index.ts new file mode 100644 index 000000000..2cb720d4f --- /dev/null +++ b/development/fitness-functions/index.ts @@ -0,0 +1,11 @@ +import { AUTOMATION_TYPE } from './common/constants'; +import { getDiffByAutomationType } from './common/get-diff'; +import { IRule, RULES, runFitnessFunctionRule } from './rules'; + +const automationType: AUTOMATION_TYPE = process.argv[2] as AUTOMATION_TYPE; + +const diff = getDiffByAutomationType(automationType); + +if (typeof diff === 'string') { + RULES.forEach((rule: IRule): void => runFitnessFunctionRule(rule, diff)); +} diff --git a/development/fitness-functions/rules/index.ts b/development/fitness-functions/rules/index.ts new file mode 100644 index 000000000..30844652c --- /dev/null +++ b/development/fitness-functions/rules/index.ts @@ -0,0 +1,42 @@ +import { preventSinonAssertSyntax } from './sinon-assert-syntax'; +import { preventJavaScriptFileAdditions } from './javascript-additions'; + +const RULES: IRule[] = [ + { + name: "Don't use `sinon` or `assert` in unit tests", + fn: preventSinonAssertSyntax, + docURL: + 'https://github.com/MetaMask/metamask-extension/blob/develop/docs/testing.md#favor-jest-instead-of-mocha', + }, + { + name: "Don't add JS or JSX files to the `shared` directory", + fn: preventJavaScriptFileAdditions, + }, +]; + +interface IRule { + name: string; + fn: (diff: string) => boolean; + docURL?: string; +} + +function runFitnessFunctionRule(rule: IRule, diff: string): void { + const { name, fn, docURL } = rule; + console.log(`Checking rule "${name}"...`); + + const hasRulePassed: boolean = fn(diff) as boolean; + if (hasRulePassed === true) { + console.log(`...OK`); + } else { + console.log(`...FAILED. Changes not accepted by the fitness function.`); + + if (docURL) { + console.log(`For more info: ${docURL}.`); + } + + process.exit(1); + } +} + +export { RULES, runFitnessFunctionRule }; +export type { IRule }; diff --git a/development/fitness-functions/rules/javascript-additions.test.ts b/development/fitness-functions/rules/javascript-additions.test.ts new file mode 100644 index 000000000..db1803c1d --- /dev/null +++ b/development/fitness-functions/rules/javascript-additions.test.ts @@ -0,0 +1,51 @@ +import { + generateModifyFilesDiff, + generateCreateFileDiff, +} from '../common/test-data'; +import { preventJavaScriptFileAdditions } from './javascript-additions'; + +describe('preventJavaScriptFileAdditions()', (): void => { + it('should pass when receiving an empty diff', (): void => { + const testDiff = ''; + + const hasRulePassed = preventJavaScriptFileAdditions(testDiff); + + expect(hasRulePassed).toBe(true); + }); + + it('should pass when receiving a diff with a new TS file on the shared folder', (): void => { + const testDiff = [ + generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), + generateModifyFilesDiff('old-file.js', undefined, 'pong'), + generateCreateFileDiff('shared/test.ts', 'yada yada yada yada'), + ].join(''); + + const hasRulePassed = preventJavaScriptFileAdditions(testDiff); + + expect(hasRulePassed).toBe(true); + }); + + it('should not pass when receiving a diff with a new JS file on the shared folder', (): void => { + const testDiff = [ + generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), + generateModifyFilesDiff('old-file.js', undefined, 'pong'), + generateCreateFileDiff('shared/test.js', 'yada yada yada yada'), + ].join(''); + + const hasRulePassed = preventJavaScriptFileAdditions(testDiff); + + expect(hasRulePassed).toBe(false); + }); + + it('should not pass when receiving a diff with a new JSX file on the shared folder', (): void => { + const testDiff = [ + generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), + generateModifyFilesDiff('old-file.js', undefined, 'pong'), + generateCreateFileDiff('shared/test.jsx', 'yada yada yada yada'), + ].join(''); + + const hasRulePassed = preventJavaScriptFileAdditions(testDiff); + + expect(hasRulePassed).toBe(false); + }); +}); diff --git a/development/fitness-functions/rules/javascript-additions.ts b/development/fitness-functions/rules/javascript-additions.ts new file mode 100644 index 000000000..3e3705ea3 --- /dev/null +++ b/development/fitness-functions/rules/javascript-additions.ts @@ -0,0 +1,18 @@ +import { SHARED_FOLDER_JS_REGEX } from '../common/constants'; +import { + filterDiffByFilePath, + filterDiffFileCreations, +} from '../common/shared'; + +function preventJavaScriptFileAdditions(diff: string): boolean { + const sharedFolderDiff = filterDiffByFilePath(diff, SHARED_FOLDER_JS_REGEX); + const sharedFolderCreationDiff = filterDiffFileCreations(sharedFolderDiff); + + const hasCreatedAtLeastOneJSFileInShared = sharedFolderCreationDiff !== ''; + if (hasCreatedAtLeastOneJSFileInShared) { + return false; + } + return true; +} + +export { preventJavaScriptFileAdditions }; diff --git a/development/fitness-functions/rules/sinon-assert-syntax.test.ts b/development/fitness-functions/rules/sinon-assert-syntax.test.ts new file mode 100644 index 000000000..ea403d4a0 --- /dev/null +++ b/development/fitness-functions/rules/sinon-assert-syntax.test.ts @@ -0,0 +1,29 @@ +import { generateModifyFilesDiff } from '../common/test-data'; +import { preventSinonAssertSyntax } from './sinon-assert-syntax'; + +describe('preventSinonAssertSyntax()', (): void => { + it('should pass when receiving an empty diff', (): void => { + const testDiff = ''; + + const hasRulePassed = preventSinonAssertSyntax(testDiff); + + expect(hasRulePassed).toBe(true); + }); + + it('should not pass when receiving a diff with one of the blocked expressions', (): void => { + const infringingExpression = 'assert.equal'; + const testDiff = [ + generateModifyFilesDiff('new-file.ts', 'foo', 'bar'), + generateModifyFilesDiff('old-file.js', undefined, 'pong'), + generateModifyFilesDiff( + 'test.js', + `yada yada ${infringingExpression} yada yada`, + undefined, + ), + ].join(''); + + const hasRulePassed = preventSinonAssertSyntax(testDiff); + + expect(hasRulePassed).toBe(false); + }); +}); diff --git a/development/fitness-functions/rules/sinon-assert-syntax.ts b/development/fitness-functions/rules/sinon-assert-syntax.ts new file mode 100644 index 000000000..57551f929 --- /dev/null +++ b/development/fitness-functions/rules/sinon-assert-syntax.ts @@ -0,0 +1,30 @@ +import { EXCLUDE_E2E_TESTS_REGEX } from '../common/constants'; +import { + filterDiffLineAdditions, + filterDiffByFilePath, + hasNumberOfCodeBlocksIncreased, +} from '../common/shared'; + +const codeBlocks = [ + "import { strict as assert } from 'assert';", + 'assert.deepEqual', + 'assert.equal', + 'assert.rejects', + 'assert.strictEqual', + 'sinon.', +]; + +function preventSinonAssertSyntax(diff: string): boolean { + const diffByFilePath = filterDiffByFilePath(diff, EXCLUDE_E2E_TESTS_REGEX); + const diffAdditions = filterDiffLineAdditions(diffByFilePath); + const hashmap = hasNumberOfCodeBlocksIncreased(diffAdditions, codeBlocks); + + const haveOccurencesOfAtLeastOneCodeBlockIncreased = + Object.values(hashmap).includes(true); + if (haveOccurencesOfAtLeastOneCodeBlockIncreased) { + return false; + } + return true; +} + +export { preventSinonAssertSyntax }; diff --git a/package.json b/package.json index fdcb71b9c..4ae6f6aac 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "test-storybook": "test-storybook -c .storybook", "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn storybook:build && npx http-server storybook-build --port 6006 \" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2\"", "githooks:install": "husky install", - "fitness-functions": "node development/fitness-functions/index.js", + "fitness-functions": "ts-node development/fitness-functions/index.ts", "generate-beta-commit": "node ./development/generate-beta-commit.js" }, "resolutions": {