diff --git a/eslint-suppressions.json b/eslint-suppressions.json new file mode 100644 index 00000000000..2e91fe99569 --- /dev/null +++ b/eslint-suppressions.json @@ -0,0 +1,293 @@ +{ + "packages/accounts-controller/src/AccountsController.test.ts": { + "import-x/namespace": { + "count": 1 + } + }, + "packages/assets-controllers/jest.environment.js": { + "n/prefer-global/text-decoder": { + "count": 1 + }, + "n/prefer-global/text-encoder": { + "count": 1 + }, + "no-shadow": { + "count": 2 + } + }, + "packages/assets-controllers/src/AccountTrackerController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "packages/assets-controllers/src/DeFiPositionsController/DeFiPositionsController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "packages/assets-controllers/src/MultichainAssetsRatesController/MultichainAssetsRatesController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "packages/assets-controllers/src/NftController.test.ts": { + "import-x/namespace": { + "count": 9 + } + }, + "packages/assets-controllers/src/NftController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "packages/assets-controllers/src/NftDetectionController.test.ts": { + "import-x/namespace": { + "count": 6 + } + }, + "packages/assets-controllers/src/Standards/ERC20Standard.test.ts": { + "jest/no-commented-out-tests": { + "count": 1 + } + }, + "packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts": { + "import-x/no-named-as-default-member": { + "count": 1 + } + }, + "packages/assets-controllers/src/TokenBalancesController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "packages/assets-controllers/src/TokenDetectionController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 5 + } + }, + "packages/assets-controllers/src/TokenListController.test.ts": { + "import-x/namespace": { + "count": 7 + } + }, + "packages/assets-controllers/src/TokensController.test.ts": { + "import-x/namespace": { + "count": 1 + } + }, + "packages/assets-controllers/src/TokensController.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 + } + }, + "packages/assets-controllers/src/multicall.test.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 2 + } + }, + "packages/base-controller/src/BaseController.test.ts": { + "import-x/namespace": { + "count": 13 + } + }, + "packages/composable-controller/src/ComposableController.test.ts": { + "import-x/namespace": { + "count": 3 + } + }, + "packages/controller-utils/jest.environment.js": { + "n/prefer-global/text-decoder": { + "count": 1 + }, + "n/prefer-global/text-encoder": { + "count": 1 + }, + "no-shadow": { + "count": 2 + } + }, + "packages/controller-utils/src/siwe.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 + } + }, + "packages/controller-utils/src/util.test.ts": { + "import-x/no-named-as-default": { + "count": 1 + }, + "promise/param-names": { + "count": 2 + } + }, + "packages/controller-utils/src/util.ts": { + "@typescript-eslint/no-base-to-string": { + "count": 1 + }, + "@typescript-eslint/no-unused-vars": { + "count": 3 + }, + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + }, + "promise/param-names": { + "count": 3 + } + }, + "packages/eip-5792-middleware/src/hooks/processSendCalls.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "packages/eth-block-tracker/tests/recordCallsToSetTimeout.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "packages/eth-block-tracker/tests/setupAfterEnv.ts": { + "@typescript-eslint/consistent-type-definitions": { + "count": 1 + }, + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "packages/eth-block-tracker/tests/withBlockTracker.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "packages/gas-fee-controller/src/GasFeeController.test.ts": { + "import-x/namespace": { + "count": 2 + } + }, + "packages/json-rpc-middleware-stream/src/index.test.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + }, + "no-empty-function": { + "count": 1 + } + }, + "packages/keyring-controller/jest.environment.js": { + "n/no-unsupported-features/node-builtins": { + "count": 1 + } + }, + "packages/keyring-controller/src/KeyringController.test.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "packages/keyring-controller/src/KeyringController.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 + } + }, + "packages/logging-controller/src/LoggingController.test.ts": { + "import-x/namespace": { + "count": 1 + } + }, + "packages/message-manager/src/utils.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 + } + }, + "packages/multichain-transactions-controller/src/MultichainTransactionsController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "packages/name-controller/src/util.ts": { + "jsdoc/require-returns": { + "count": 1 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "packages/phishing-controller/src/utils.test.ts": { + "import-x/namespace": { + "count": 5 + } + }, + "packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.test.ts": { + "promise/param-names": { + "count": 1 + } + }, + "packages/sample-controllers/src/sample-gas-prices-controller.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "packages/sample-controllers/src/sample-gas-prices-service/sample-gas-prices-service.test.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "packages/seedless-onboarding-controller/jest.environment.js": { + "n/no-unsupported-features/node-builtins": { + "count": 1 + } + }, + "packages/signature-controller/src/utils/normalize.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 + } + }, + "packages/signature-controller/src/utils/validation.ts": { + "@typescript-eslint/no-base-to-string": { + "count": 1 + }, + "@typescript-eslint/no-unused-vars": { + "count": 2 + } + }, + "packages/user-operation-controller/src/UserOperationController.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + }, + "jsdoc/require-returns": { + "count": 2 + } + }, + "packages/user-operation-controller/src/helpers/Bundler.test.ts": { + "jsdoc/require-returns": { + "count": 1 + } + }, + "scripts/create-package/utils.test.ts": { + "import-x/no-named-as-default-member": { + "count": 2 + } + }, + "tests/fake-block-tracker.ts": { + "no-empty-function": { + "count": 1 + } + }, + "tests/fake-provider.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + } + }, + "tests/setupAfterEnv/nock.ts": { + "import-x/no-named-as-default-member": { + "count": 3 + } + } +} diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json deleted file mode 100644 index 588ec3d007c..00000000000 --- a/eslint-warning-thresholds.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "packages/accounts-controller/src/AccountsController.test.ts": { - "import-x/namespace": 1 - }, - "packages/assets-controllers/jest.environment.js": { - "n/prefer-global/text-encoder": 1, - "n/prefer-global/text-decoder": 1, - "no-shadow": 2 - }, - "packages/assets-controllers/src/AccountTrackerController.ts": { - "@typescript-eslint/no-misused-promises": 4 - }, - "packages/assets-controllers/src/DeFiPositionsController/DeFiPositionsController.ts": { - "@typescript-eslint/no-misused-promises": 2 - }, - "packages/assets-controllers/src/MultichainAssetsController/MultichainAssetsController.ts": { - "@typescript-eslint/no-misused-promises": 3 - }, - "packages/assets-controllers/src/MultichainAssetsRatesController/MultichainAssetsRatesController.ts": { - "@typescript-eslint/no-misused-promises": 2 - }, - "packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts": { - "@typescript-eslint/no-misused-promises": 2 - }, - "packages/assets-controllers/src/NftController.test.ts": { - "import-x/namespace": 9 - }, - "packages/assets-controllers/src/NftController.ts": { - "@typescript-eslint/no-misused-promises": 2 - }, - "packages/assets-controllers/src/NftDetectionController.test.ts": { - "import-x/namespace": 6 - }, - "packages/assets-controllers/src/Standards/ERC20Standard.test.ts": { - "jest/no-commented-out-tests": 1 - }, - "packages/assets-controllers/src/Standards/NftStandards/ERC1155/ERC1155Standard.test.ts": { - "import-x/no-named-as-default-member": 1 - }, - "packages/assets-controllers/src/TokenBalancesController.ts": { - "@typescript-eslint/no-misused-promises": 1 - }, - "packages/assets-controllers/src/TokenDetectionController.ts": { - "@typescript-eslint/no-misused-promises": 5 - }, - "packages/assets-controllers/src/TokenListController.test.ts": { - "import-x/namespace": 7 - }, - "packages/assets-controllers/src/TokensController.test.ts": { - "import-x/namespace": 1 - }, - "packages/assets-controllers/src/TokensController.ts": { - "@typescript-eslint/no-unused-vars": 1 - }, - "packages/assets-controllers/src/multicall.test.ts": { - "@typescript-eslint/prefer-promise-reject-errors": 2 - }, - "packages/base-controller/src/BaseController.test.ts": { - "import-x/namespace": 13 - }, - "packages/composable-controller/src/ComposableController.test.ts": { - "import-x/namespace": 3 - }, - "packages/controller-utils/jest.environment.js": { - "n/prefer-global/text-encoder": 1, - "n/prefer-global/text-decoder": 1, - "no-shadow": 2 - }, - "packages/controller-utils/src/siwe.ts": { - "@typescript-eslint/no-unused-vars": 1 - }, - "packages/controller-utils/src/util.test.ts": { - "import-x/no-named-as-default": 1, - "promise/param-names": 2 - }, - "packages/controller-utils/src/util.ts": { - "@typescript-eslint/no-base-to-string": 1, - "@typescript-eslint/no-unused-vars": 3, - "@typescript-eslint/prefer-promise-reject-errors": 1, - "promise/param-names": 3 - }, - "packages/eip-5792-middleware/src/hooks/processSendCalls.ts": { - "@typescript-eslint/no-misused-promises": 1 - }, - "packages/eth-block-tracker/src/PollingBlockTracker.test.ts": { - "@typescript-eslint/unbound-method": 4 - }, - "packages/eth-block-tracker/src/PollingBlockTracker.ts": { - "@typescript-eslint/prefer-nullish-coalescing": 6, - "@typescript-eslint/unbound-method": 5, - "no-restricted-syntax": 28 - }, - "packages/eth-block-tracker/tests/recordCallsToSetTimeout.ts": { - "@typescript-eslint/no-explicit-any": 1 - }, - "packages/eth-block-tracker/tests/setupAfterEnv.ts": { - "@typescript-eslint/consistent-type-definitions": 1, - "@typescript-eslint/no-explicit-any": 3 - }, - "packages/eth-block-tracker/tests/withBlockTracker.ts": { - "@typescript-eslint/no-explicit-any": 1 - }, - "packages/gas-fee-controller/src/GasFeeController.test.ts": { - "import-x/namespace": 2 - }, - "packages/json-rpc-middleware-stream/src/index.test.ts": { - "@typescript-eslint/prefer-promise-reject-errors": 1, - "no-empty-function": 1 - }, - "packages/keyring-controller/jest.environment.js": { - "n/no-unsupported-features/node-builtins": 1 - }, - "packages/keyring-controller/src/KeyringController.test.ts": { - "@typescript-eslint/no-misused-promises": 1 - }, - "packages/keyring-controller/src/KeyringController.ts": { - "@typescript-eslint/no-unused-vars": 1 - }, - "packages/logging-controller/src/LoggingController.test.ts": { - "import-x/namespace": 1 - }, - "packages/message-manager/src/utils.ts": { - "@typescript-eslint/no-unused-vars": 1 - }, - "packages/multichain-transactions-controller/src/MultichainTransactionsController.ts": { - "@typescript-eslint/no-misused-promises": 2 - }, - "packages/name-controller/src/util.ts": { - "jsdoc/require-returns": 1 - }, - "packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts": { - "@typescript-eslint/no-misused-promises": 1 - }, - "packages/phishing-controller/src/utils.test.ts": { - "import-x/namespace": 5 - }, - "packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.test.ts": { - "promise/param-names": 1 - }, - "packages/sample-controllers/src/sample-gas-prices-controller.ts": { - "@typescript-eslint/no-misused-promises": 1 - }, - "packages/sample-controllers/src/sample-gas-prices-service/sample-gas-prices-service.test.ts": { - "@typescript-eslint/no-misused-promises": 4 - }, - "packages/seedless-onboarding-controller/jest.environment.js": { - "n/no-unsupported-features/node-builtins": 1 - }, - "packages/signature-controller/src/utils/normalize.ts": { - "@typescript-eslint/no-unused-vars": 1 - }, - "packages/signature-controller/src/utils/validation.ts": { - "@typescript-eslint/no-base-to-string": 1, - "@typescript-eslint/no-unused-vars": 2 - }, - "packages/user-operation-controller/src/UserOperationController.ts": { - "@typescript-eslint/prefer-promise-reject-errors": 1, - "jsdoc/require-returns": 2 - }, - "packages/user-operation-controller/src/helpers/Bundler.test.ts": { - "jsdoc/require-returns": 1 - }, - "scripts/create-package/utils.test.ts": { - "import-x/no-named-as-default-member": 2 - }, - "tests/fake-block-tracker.ts": { - "no-empty-function": 1 - }, - "tests/fake-provider.ts": { - "@typescript-eslint/prefer-promise-reject-errors": 1 - }, - "tests/setupAfterEnv/nock.ts": { - "import-x/no-named-as-default-member": 3 - } -} diff --git a/eslint.config.mjs b/eslint.config.mjs index 94942d38272..423708baed6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -45,12 +45,6 @@ const config = createConfig([ 'off', { matchDescription: '^[A-Z`\\d_][\\s\\S]*[.?!`>)}]$' }, ], - - // TODO: These rules created more errors after the upgrade to ESLint 9. - // Re-enable these rules and address any lint violations. - 'import-x/no-named-as-default-member': 'warn', - 'prettier/prettier': 'warn', - 'no-empty-function': 'warn', }, settings: { jsdoc: { @@ -71,19 +65,17 @@ const config = createConfig([ rules: { // TODO: Re-enable this 'n/no-sync': 'off', - // TODO: These rules created more errors after the upgrade to ESLint 9. - // Re-enable these rules and address any lint violations. - 'n/no-unsupported-features/node-builtins': 'warn', }, }, { files: ['**/*.test.{js,ts}', '**/tests/**/*.{js,ts}'], extends: [jest], rules: { - // TODO: These rules created more errors after the upgrade to ESLint 9. - // Re-enable these rules and address any lint violations. - 'jest/prefer-lowercase-title': 'warn', - 'jest/prefer-strict-equal': 'warn', + // TODO: Upgrade these from warning to error in shared config + 'jest/expect-expect': 'error', + 'jest/no-alias-methods': 'error', + 'jest/no-commented-out-tests': 'error', + 'jest/no-disabled-tests': 'error', // TODO: Re-enable this rule 'jest/unbound-method': 'off', @@ -144,6 +136,11 @@ const config = createConfig([ // TODO: auto-fix breaks stuff '@typescript-eslint/promise-function-async': 'off', + // TODO: Re-enable this rule + // Enabling it with error suppression breaks `--fix`, because the autofixer for this rule + // does not work very well. + 'jsdoc/check-tag-names': 'off', + // TODO: re-enable most of these rules '@typescript-eslint/naming-convention': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', @@ -154,27 +151,7 @@ const config = createConfig([ '@typescript-eslint/prefer-reduce-type-parameter': 'off', 'no-restricted-syntax': 'off', 'no-restricted-globals': 'off', - - // TODO: These rules created more errors after the upgrade to ESLint 9. - // Re-enable these rules and address any lint violations. - '@typescript-eslint/consistent-type-exports': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-base-to-string': 'warn', - '@typescript-eslint/no-duplicate-enum-values': 'warn', - '@typescript-eslint/no-misused-promises': 'warn', - '@typescript-eslint/no-unused-vars': 'warn', - '@typescript-eslint/only-throw-error': 'warn', - '@typescript-eslint/prefer-promise-reject-errors': 'warn', - '@typescript-eslint/prefer-readonly': 'warn', - 'import-x/namespace': 'warn', - 'import-x/no-named-as-default': 'warn', - 'import-x/order': 'warn', - 'jsdoc/require-returns': 'warn', - 'jsdoc/tag-lines': 'warn', - 'no-unused-private-class-members': 'warn', - 'promise/always-return': 'warn', - 'promise/catch-or-return': 'warn', - 'promise/param-names': 'warn', }, }, { @@ -202,12 +179,6 @@ const config = createConfig([ rules: { // These files run under Node, and thus `require(...)` is expected. 'n/global-require': 'off', - - // TODO: These rules created more errors after the upgrade to ESLint 9. - // Re-enable these rules and address any lint violations. - 'n/prefer-global/text-encoder': 'warn', - 'n/prefer-global/text-decoder': 'warn', - 'no-shadow': 'warn', }, }, { @@ -216,17 +187,6 @@ const config = createConfig([ sourceType: 'module', }, }, - { - files: ['packages/eth-block-tracker/**/*.ts'], - rules: { - // TODO: Re-enable these rules or add inline ignores for warranted cases - '@typescript-eslint/prefer-nullish-coalescing': 'warn', - 'no-restricted-syntax': 'warn', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/unbound-method': 'warn', - '@typescript-eslint/consistent-type-definitions': 'warn', - }, - }, { files: ['packages/foundryup/**/*.{js,ts}'], rules: { diff --git a/package.json b/package.json index 510d4bfbe68..2edd96cda51 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "lint": "yarn lint:eslint && echo && yarn lint:misc --check && yarn constraints && yarn lint:dependencies && yarn lint:teams && yarn generate-method-action-types --check", "lint:dependencies": "depcheck && yarn dedupe --check", "lint:dependencies:fix": "depcheck && yarn dedupe", - "lint:eslint": "yarn build:only-clean && yarn tsx ./scripts/run-eslint.ts ", - "lint:fix": "yarn lint:eslint --fix && echo && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies:fix && yarn generate-method-action-types --fix", + "lint:eslint": "yarn build:only-clean && yarn eslint", + "lint:fix": "yarn lint:eslint --fix --prune-suppressions && echo && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies:fix && yarn generate-method-action-types --fix", "lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path .gitignore", "lint:teams": "tsx scripts/lint-teams-json.ts", "prepack": "./scripts/prepack.sh", @@ -74,7 +74,6 @@ "@typescript-eslint/parser": "^8.7.0", "@yarnpkg/types": "^4.0.0", "babel-jest": "^29.7.0", - "chalk": "^4.1.2", "depcheck": "^1.4.7", "eslint": "^9.39.1", "eslint-config-prettier": "^9.1.0", diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts index 5ca867a94c9..6e88d18e507 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification-api/schema.ts @@ -1,5 +1,5 @@ /* eslint-disable jsdoc/tag-lines */ -/* eslint-disable jsdoc/check-tag-names */ + /** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. diff --git a/scripts/run-eslint.ts b/scripts/run-eslint.ts deleted file mode 100644 index b0f0fe6e2ca..00000000000 --- a/scripts/run-eslint.ts +++ /dev/null @@ -1,519 +0,0 @@ -import chalk from 'chalk'; -import { ESLint } from 'eslint'; -import fs from 'fs'; -import path from 'path'; -import yargs from 'yargs'; - -const PROJECT_DIRECTORY = path.resolve(__dirname, '..'); - -const WARNING_THRESHOLDS_FILE = path.join( - PROJECT_DIRECTORY, - 'eslint-warning-thresholds.json', -); - -/** - * The parsed command-line arguments. - */ -type CommandLineArguments = { - /** - * Whether to cache results to speed up future runs (true) or not (false). - */ - cache: boolean; - /** - * A list of specific files to lint. - */ - files: string[]; - /** - * Whether to automatically fix lint errors (true) or not (false). - */ - fix: boolean; - /** - * Whether to only report errors, disabling the warnings quality gate in the - * process (true) or not (false). - */ - quiet: boolean; -}; - -/** - * A two-level object mapping path to files in which warnings appear to the IDs - * of rules for those warnings, then from rule IDs to the number of warnings for - * the rule. - * - * @example - * ``` ts - * { - * "foo.ts": { - * "rule1": 3, - * "rule2": 4 - * }, - * "bar.ts": { - * "rule3": 17, - * "rule4": 5 - * } - * } - * ``` - */ -type WarningCounts = Record>; - -/** - * An object indicating the difference in warnings for a specific rule. - */ -type WarningComparison = { - /** The file path of the ESLint rule. */ - filePath: string; - /** The ID of the ESLint rule. */ - ruleId: string; - /** The previous count of warnings for the rule. */ - threshold: number; - /** The current count of warnings for the rule. */ - count: number; - /** The difference between the count and the threshold for the rule. */ - difference: number; -}; - -/** - * The severity level for an ESLint message. - */ -const ESLintMessageSeverity = { - Warning: 1, - Error: 2, -} as const; - -/** - * The result of applying the quality gate. - */ -const QualityGateStatus = { - /** - * The number of lint warnings increased. - */ - Increase: 'increase', - /** - * The number of lint warnings decreased. - */ - Decrease: 'decrease', - /** - * There was no change to the number of lint warnings. - */ - NoChange: 'no-change', - /** - * The warning thresholds file did not previously exist. - */ - Initialized: 'initialized', -} as const; - -/** - * The result of applying the quality gate. - */ -type QualityGateStatus = - (typeof QualityGateStatus)[keyof typeof QualityGateStatus]; - -// Run the script. -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); - -/** - * The entrypoint to this script. - */ -async function main() { - const { - cache, - fix, - files: givenFiles, - quiet, - } = await parseCommandLineArguments(); - - const eslint = new ESLint({ - cache, - errorOnUnmatchedPattern: false, - fix, - ruleFilter: ({ severity }) => - !quiet || severity === ESLintMessageSeverity.Error, - }); - - const fileFilteredResults = await eslint.lintFiles( - givenFiles.length > 0 ? givenFiles : ['.'], - ); - - const filteredResults = quiet - ? ESLint.getErrorResults(fileFilteredResults) - : fileFilteredResults; - - await printResults(eslint, filteredResults); - - if (fix) { - await ESLint.outputFixes(filteredResults); - } - const hasErrors = filteredResults.some((result) => result.errorCount > 0); - - const qualityGateStatus = applyWarningThresholdsQualityGate(filteredResults); - - if (hasErrors || qualityGateStatus === QualityGateStatus.Increase) { - process.exitCode = 1; - } -} - -/** - * Uses `yargs` to parse the arguments given to the script. - * - * @returns The parsed arguments. - */ -async function parseCommandLineArguments(): Promise { - const { cache, fix, quiet, ...rest } = await yargs(process.argv.slice(2)) - .option('cache', { - type: 'boolean', - description: 'Cache results to speed up future runs', - default: false, - }) - .option('fix', { - type: 'boolean', - description: - 'Automatically fix all problems; pair with --quiet to only fix errors', - default: false, - }) - .option('quiet', { - type: 'boolean', - description: 'Only report or fix errors', - default: false, - }) - .help() - .string('_').argv; - - // Type assertion: The types for `yargs`'s `string` method are wrong. - const files = rest._ as string[]; - - return { cache, fix, quiet, files }; -} - -/** - * Uses the given results to print the output that `eslint` usually generates. - * - * @param eslint - The ESLint instance. - * @param results - The results from running `eslint`. - */ -async function printResults( - eslint: ESLint, - results: ESLint.LintResult[], -): Promise { - const formatter = await eslint.loadFormatter('stylish'); - const resultText = await formatter.format(results); - if (resultText.length > 0) { - console.log(resultText); - } -} - -/** - * This function represents the ESLint warnings quality gate, which will cause - * linting to pass or fail depending on how many new warnings have been - * produced. - * - * - If we have no record of warnings from a previous run, then we simply - * capture the new warnings in a file and continue. - * - If we have a record of warnings from a previous run and there are any - * changes to the number of warnings overall, then we list which ESLint rules - * had increases and decreases. If are were more warnings overall then we fail, - * otherwise we pass. - * - * @param results - The results from running `eslint`. - * @returns True if the number of warnings has increased compared to the - * existing number of warnings, false if they have decreased or stayed the same. - */ -function applyWarningThresholdsQualityGate( - results: ESLint.LintResult[], -): QualityGateStatus { - const warningThresholds = loadWarningThresholds(); - const warningCounts = getWarningCounts(results); - - const completeWarningCounts = removeFilesWithoutWarnings({ - ...warningThresholds, - ...warningCounts, - }); - - let status; - - if (Object.keys(warningThresholds).length === 0) { - console.log( - chalk.blue( - 'The following lint violations were produced and will be captured as thresholds for future runs:\n', - ), - ); - - for (const [filePath, ruleCounts] of Object.entries( - completeWarningCounts, - )) { - console.log(chalk.underline(filePath)); - for (const [ruleId, count] of Object.entries(ruleCounts)) { - console.log(` ${chalk.cyan(ruleId)}: ${count}`); - } - } - - saveWarningThresholds(completeWarningCounts); - - status = QualityGateStatus.Initialized; - } else { - const comparisonsByFile = compareWarnings( - warningThresholds, - completeWarningCounts, - ); - - const changes = Object.values(comparisonsByFile) - .flat() - .filter((comparison) => comparison.difference !== 0); - const regressions = Object.values(comparisonsByFile) - .flat() - .filter((comparison) => comparison.difference > 0); - - if (changes.length > 0) { - if (regressions.length > 0) { - console.log( - chalk.red( - '🛑 New lint violations have been introduced and need to be resolved for linting to pass:\n', - ), - ); - - for (const [filePath, fileChanges] of Object.entries( - comparisonsByFile, - )) { - if (fileChanges.some((fileChange) => fileChange.difference > 0)) { - console.log(chalk.underline(filePath)); - for (const { - ruleId, - threshold, - count, - difference, - } of fileChanges) { - if (difference > 0) { - console.log( - ` ${chalk.cyan(ruleId)}: ${threshold} -> ${count} (${difference > 0 ? chalk.red(`+${difference}`) : chalk.green(difference)})`, - ); - } - } - } - } - - status = QualityGateStatus.Increase; - } else { - console.log( - chalk.green( - 'The overall number of lint warnings has decreased, good work! ❤️ \n', - ), - ); - - for (const [filePath, fileChanges] of Object.entries( - comparisonsByFile, - )) { - if (fileChanges.some((fileChange) => fileChange.difference !== 0)) { - console.log(chalk.underline(filePath)); - for (const { - ruleId, - threshold, - count, - difference, - } of fileChanges) { - if (difference !== 0) { - console.log( - ` ${chalk.cyan(ruleId)}: ${threshold} -> ${count} (${difference > 0 ? chalk.red(`+${difference}`) : chalk.green(difference)})`, - ); - } - } - } - } - - console.log( - `\n${chalk.yellow.bold(path.basename(WARNING_THRESHOLDS_FILE))}${chalk.yellow(' has been updated with the new counts. Please make sure to commit the changes.')}`, - ); - - saveWarningThresholds(completeWarningCounts); - - status = QualityGateStatus.Decrease; - } - } else { - status = QualityGateStatus.NoChange; - } - } - - return status; -} - -/** - * Removes properties from the given warning counts object that have no warnings. - * - * @param warningCounts - The warning counts. - * @returns The transformed warning counts. - */ -function removeFilesWithoutWarnings(warningCounts: WarningCounts) { - return Object.entries(warningCounts).reduce( - (newWarningCounts: WarningCounts, [filePath, warnings]) => { - if (Object.keys(warnings).length === 0) { - return newWarningCounts; - } - return { ...newWarningCounts, [filePath]: warnings }; - }, - {}, - ); -} - -/** - * Loads previous warning thresholds from a file. - * - * @returns The warning thresholds loaded from file. - */ -function loadWarningThresholds(): WarningCounts { - if (fs.existsSync(WARNING_THRESHOLDS_FILE)) { - const data = fs.readFileSync(WARNING_THRESHOLDS_FILE, 'utf-8'); - return JSON.parse(data); - } - return {}; -} - -/** - * Saves current warning counts to a file so they can be referenced in a future - * run. - * - * @param newWarningCounts - The new warning thresholds to save. - */ -function saveWarningThresholds(newWarningCounts: WarningCounts): void { - fs.writeFileSync( - WARNING_THRESHOLDS_FILE, - `${JSON.stringify(newWarningCounts, null, 2)}\n`, - 'utf-8', - ); -} - -/** - * Given a list of results from an the ESLint run, counts the number of warnings - * produced per file and rule. - * - * @param results - The results from running `eslint`. - * @returns A two-level object mapping path to files in which warnings appear to - * the IDs of rules for those warnings, then from rule IDs to the number of - * warnings for the rule. - */ -function getWarningCounts(results: ESLint.LintResult[]): WarningCounts { - const unsortedWarningCounts = results.reduce( - (workingWarningCounts, result) => { - const { filePath } = result; - const relativeFilePath = path.relative(PROJECT_DIRECTORY, filePath); - if (!workingWarningCounts[relativeFilePath]) { - workingWarningCounts[relativeFilePath] = {}; - } - for (const message of result.messages) { - if ( - message.severity === ESLintMessageSeverity.Warning && - message.ruleId - ) { - workingWarningCounts[relativeFilePath][message.ruleId] = - (workingWarningCounts[relativeFilePath][message.ruleId] ?? 0) + 1; - } - } - return workingWarningCounts; - }, - {} as WarningCounts, - ); - - const sortedWarningCounts: WarningCounts = {}; - for (const filePath of Object.keys(unsortedWarningCounts).sort()) { - // We can safely assume this property is present. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const unsortedWarningCountsForFile = unsortedWarningCounts[filePath]!; - sortedWarningCounts[filePath] = Object.keys(unsortedWarningCountsForFile) - .sort(sortRules) - .reduce( - (acc, ruleId) => { - // We can safely assume this property is present. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - acc[ruleId] = unsortedWarningCountsForFile[ruleId]!; - return acc; - }, - {} as Record, - ); - } - return sortedWarningCounts; -} - -/** - * Compares previous and current warning counts. - * - * @param warningThresholds - The previously recorded warning thresholds - * (organized by file and then rule). - * @param warningCounts - The current warning counts (organized by file and then - * rule). - * @returns An object mapping file paths to arrays of objects indicating - * comparisons in warnings. - */ -function compareWarnings( - warningThresholds: WarningCounts, - warningCounts: WarningCounts, -): Record { - const comparisons: Record = {}; - const filePaths = Array.from( - new Set([...Object.keys(warningThresholds), ...Object.keys(warningCounts)]), - ); - - for (const filePath of filePaths) { - const ruleIds = Array.from( - new Set([ - ...Object.keys(warningThresholds[filePath] || {}), - ...Object.keys(warningCounts[filePath] || {}), - ]), - ); - - comparisons[filePath] = ruleIds - .map((ruleId) => { - const threshold = warningThresholds[filePath]?.[ruleId] ?? 0; - const count = warningCounts[filePath]?.[ruleId] ?? 0; - const difference = count - threshold; - return { filePath, ruleId, threshold, count, difference }; - }) - .sort((a, b) => sortRules(a.ruleId, b.ruleId)); - } - - return Object.keys(comparisons) - .sort() - .reduce( - (sortedComparisons, filePath) => { - // We can safely assume this property is present. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - sortedComparisons[filePath] = comparisons[filePath]!; - return sortedComparisons; - }, - {} as Record, - ); -} - -/** - * Sorts rule IDs, ensuring that rules with namespaces appear before rules - * without. - * - * @param ruleIdA - The first rule ID. - * @param ruleIdB - The second rule ID. - * @returns A negative number if the first rule ID should come before the - * second, a positive number if the first should come _after_ the second, or 0 - * if they should stay where they are. - * @example - * ``` typescript - * sortRules( - * '@typescript-eslint/naming-convention', - * '@typescript-eslint/explicit-function-return-type' - * ) //=> 1 (sort A after B) - * sortRules( - * 'explicit-function-return-type', - * '@typescript-eslint/naming-convention' - * ) //=> 1 (sort A after B) - */ -function sortRules(ruleIdA: string, ruleIdB: string): number { - const [namespaceA, ruleA] = ruleIdA.includes('/') - ? ruleIdA.split('/') - : ['', ruleIdA]; - const [namespaceB, ruleB] = ruleIdB.includes('/') - ? ruleIdB.split('/') - : ['', ruleIdB]; - if (namespaceA && !namespaceB) { - return -1; - } - if (!namespaceA && namespaceB) { - return 1; - } - return namespaceA.localeCompare(namespaceB) || ruleA.localeCompare(ruleB); -} diff --git a/yarn.lock b/yarn.lock index b6e8e39bf13..2026883d34f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3093,7 +3093,6 @@ __metadata: "@typescript-eslint/parser": "npm:^8.7.0" "@yarnpkg/types": "npm:^4.0.0" babel-jest: "npm:^29.7.0" - chalk: "npm:^4.1.2" depcheck: "npm:^1.4.7" eslint: "npm:^9.39.1" eslint-config-prettier: "npm:^9.1.0"