diff --git a/build-tests/eslint-bulk-suppressions-test/.eslint-bulk-suppressions.json b/build-tests/eslint-bulk-suppressions-test/.eslint-bulk-suppressions.json deleted file mode 100644 index 6f649ee52de..00000000000 --- a/build-tests/eslint-bulk-suppressions-test/.eslint-bulk-suppressions.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "suppressions": [ - { - "file": "src/test-lab.ts", - "scope": ".", - "rule": "@typescript-eslint/no-namespace" - }, - { - "file": "src/test-lab.ts", - "scope": ".", - "rule": "header/header" - }, - { - "file": "src/test-lab.ts", - "scope": ".", - "rule": "prefer-const" - }, - { - "file": "src/test-lab.ts", - "scope": ".AbsurdClass.absurdClassMethod", - "rule": "@typescript-eslint/explicit-function-return-type" - }, - { - "file": "src/test-lab.ts", - "scope": ".AbsurdClass.absurdClassMethod", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".AbsurdClass.absurdClassMethod.AbsurdClass2", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".AbsurdClass.absurdClassMethod.AbsurdClass2", - "rule": "@typescript-eslint/typedef" - }, - { - "file": "src/test-lab.ts", - "scope": ".AbsurdClass.absurdClassMethod.AbsurdClass2.constructor", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".AbsurdClass.absurdClassMethod.AbsurdClass2.constructor.absurdObject", - "rule": "@typescript-eslint/typedef" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleClass", - "rule": "@typescript-eslint/ban-types" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleClass", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleClass.exampleMethod", - "rule": "@typescript-eslint/explicit-function-return-type" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleClass.exampleMethod", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleClass.exampleMethod", - "rule": "@typescript-eslint/typedef" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleClass.exampleMethod", - "rule": "no-var" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleEnum", - "rule": "dot-notation" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleInterface", - "rule": "@typescript-eslint/naming-convention" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleInterface2", - "rule": "@typescript-eslint/naming-convention" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleObjectType", - "rule": "@typescript-eslint/ban-types" - }, - { - "file": "src/test-lab.ts", - "scope": ".ExampleObjectType", - "rule": "@typescript-eslint/consistent-type-definitions" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleAnonymousClass", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleAnonymousClass", - "rule": "@typescript-eslint/typedef" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleAnonymousClass", - "rule": "no-useless-concat" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleAnonymousClass.constructor", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleAnonymousClass.exampleSetGet", - "rule": "@typescript-eslint/ban-types" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleAnonymousClass.exampleSetGet", - "rule": "@typescript-eslint/explicit-function-return-type" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleAnonymousClass.exampleSetGet", - "rule": "@typescript-eslint/explicit-member-accessibility" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleArrowFunction", - "rule": "@typescript-eslint/explicit-function-return-type" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleArrowFunction", - "rule": "@typescript-eslint/typedef" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleArrowFunction", - "rule": "dot-notation" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleArrowFunction", - "rule": "no-empty" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleArrowFunction", - "rule": "no-unused-expressions" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleFunction", - "rule": "@typescript-eslint/ban-types" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleFunction", - "rule": "@typescript-eslint/explicit-function-return-type" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleFunction", - "rule": "no-empty-pattern" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleFunction", - "rule": "no-extra-boolean-cast" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleObject", - "rule": "@typescript-eslint/typedef" - }, - { - "file": "src/test-lab.ts", - "scope": ".exampleObject2", - "rule": "@typescript-eslint/typedef" - }, - { - "file": "src/test-lab.ts", - "scope": ".x", - "rule": "@typescript-eslint/explicit-function-return-type" - }, - { - "file": "src/test-lab.ts", - "scope": ".y", - "rule": "@typescript-eslint/explicit-function-return-type" - }, - { - "file": "src/test-lab.ts", - "scope": ".z", - "rule": "@typescript-eslint/explicit-function-return-type" - } - ] -} diff --git a/build-tests/eslint-bulk-suppressions-test/.eslintrc.js b/build-tests/eslint-bulk-suppressions-test/.eslintrc.js index 9077adeabc7..f053ebf7976 100644 --- a/build-tests/eslint-bulk-suppressions-test/.eslintrc.js +++ b/build-tests/eslint-bulk-suppressions-test/.eslintrc.js @@ -1,31 +1 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -// This is a workaround for https://github.com/eslint/eslint/issues/3458 -require('local-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution'); -// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 -require('local-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names'); -require('local-node-rig/profiles/default/includes/eslint/patch/eslint-bulk-suppressions'); - -module.exports = { - extends: [ - 'local-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool', - 'local-node-rig/profiles/default/includes/eslint/mixins/friendly-locals' - ], - parserOptions: { tsconfigRootDir: __dirname }, - - overrides: [ - /** - * Override the parser from @rushstack/eslint-config. Since the config is coming - * from the workspace instead of the external NPM package, the versions of ESLint - * and TypeScript that the config consumes will be resolved from the devDependencies - * of the config instead of from the eslint-8-test package. Overriding the parser - * ensures that the these dependencies come from the eslint-8-test package. See: - * https://github.com/microsoft/rushstack/issues/3021 - */ - { - files: ['*.ts', '*.tsx'], - parser: '@typescript-eslint/parser' - } - ] -}; +module.exports = {}; diff --git a/build-tests/eslint-bulk-suppressions-test/client/.eslint-bulk-suppressions.json b/build-tests/eslint-bulk-suppressions-test/client/.eslint-bulk-suppressions.json new file mode 100644 index 00000000000..da846f80fe7 --- /dev/null +++ b/build-tests/eslint-bulk-suppressions-test/client/.eslint-bulk-suppressions.json @@ -0,0 +1,14 @@ +{ + "suppressions": [ + { + "file": "src/index.ts", + "scope": ".", + "rule": "prefer-const" + }, + { + "file": "src/index.ts", + "scope": ".exampleFunction", + "rule": "@typescript-eslint/ban-types" + } + ] +} diff --git a/build-tests/eslint-bulk-suppressions-test/client/.eslintrc.js b/build-tests/eslint-bulk-suppressions-test/client/.eslintrc.js new file mode 100644 index 00000000000..7d32f3e4d32 --- /dev/null +++ b/build-tests/eslint-bulk-suppressions-test/client/.eslintrc.js @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('local-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution'); +// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 +require('local-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names'); +require('local-node-rig/profiles/default/includes/eslint/patch/eslint-bulk-suppressions'); + +module.exports = { + extends: [ + 'plugin:@typescript-eslint/recommended' + // 'local-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool', + // 'local-node-rig/profiles/default/includes/eslint/mixins/friendly-locals' + ], + ignorePatterns: ['.eslintrc.js'], + + overrides: [ + /** + * Override the parser from @rushstack/eslint-config. Since the config is coming + * from the workspace instead of the external NPM package, the versions of ESLint + * and TypeScript that the config consumes will be resolved from the devDependencies + * of the config instead of from the eslint-8-test package. Overriding the parser + * ensures that the these dependencies come from the eslint-8-test package. See: + * https://github.com/microsoft/rushstack/issues/3021 + */ + { + files: ['**/*.ts', '**/*.tsx'], + parser: '@typescript-eslint/parser', + parserOptions: { project: '../tsconfig.json', tsconfigRootDir: __dirname } + } + ] +}; diff --git a/build-tests/eslint-bulk-suppressions-test/client/src/index.ts b/build-tests/eslint-bulk-suppressions-test/client/src/index.ts new file mode 100644 index 00000000000..fbeb70d2870 --- /dev/null +++ b/build-tests/eslint-bulk-suppressions-test/client/src/index.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/* Top-level scope code samples */ +// scopeId: '.' +let exampleString: string = 5 + ''; + +const exampleObject = { + exampleString: exampleString +}; + +/* Function scope code samples */ +export function exampleFunction() { + const {}: Object = exampleObject; + + // scopeId: '.exampleFunction' + !!!exampleString as Boolean; +} + +// scope: '.ArrowFunctionExpression', +// export const x = () => {}, +// // scopeId: '.y' +// y = () => {}, +// // scopeId: '.z' +// z = () => {}; + +// /* Class scope code samples */ +// export class ExampleClass { +// // scopeId: '.ExampleClass' +// exampleClassProperty: String = exampleString + '4'; + +// exampleMethod() { +// // scopeId: '.exampleClass.exampleMethod' +// var exampleVar; +// return exampleVar; +// } +// } + +// /* Variable and anonymous constructs code samples */ +// export const exampleArrowFunction = () => { +// const exampleBoolean = true; +// if (exampleBoolean) { +// } + +// exampleObject['exampleString']; +// }; + +// export const exampleAnonymousClass = class { +// exampleClassProperty = 'x' + 'y'; + +// // scopeId: '.exampleAnonymousClass.constructor' +// constructor() {} + +// set exampleSetGet(val: string) { +// // scopeId: '.exampleAnonymousClass.exampleSetGet' +// let exampleVariable: Number = 1; +// this.exampleClassProperty = val + exampleVariable; +// } + +// get exampleSetGet() { +// // scopeId: '.exampleAnonymousClass.exampleSetGet' +// return this.exampleClassProperty as String as string; +// } +// }; diff --git a/build-tests/eslint-bulk-suppressions-test/package.json b/build-tests/eslint-bulk-suppressions-test/package.json index d3e9d07ecdc..97b874a5ed6 100644 --- a/build-tests/eslint-bulk-suppressions-test/package.json +++ b/build-tests/eslint-bulk-suppressions-test/package.json @@ -6,7 +6,8 @@ "scripts": { "eslint": "eslint .", "eslint-bulk-suppress-all": "ESLINT_BULK_SUPPRESS_RULES=* eslint .", - "eslint-without-bulk-suppressions": "USE_ESLINT_BULK_SUPPRESSIONS=false eslint ." + "eslint-without-bulk-suppressions": "USE_ESLINT_BULK_SUPPRESSIONS=false eslint .", + "debug": "CLEANUP_ESLINT_BULK_SUPPRESSIONS=true eslint /Users/bytedance/rushstack/build-tests/eslint-bulk-suppressions-test/server" }, "devDependencies": { "@rushstack/heft": "workspace:*", diff --git a/build-tests/eslint-bulk-suppressions-test/server/.eslint-bulk-suppressions.json b/build-tests/eslint-bulk-suppressions-test/server/.eslint-bulk-suppressions.json new file mode 100644 index 00000000000..6f2aa101494 --- /dev/null +++ b/build-tests/eslint-bulk-suppressions-test/server/.eslint-bulk-suppressions.json @@ -0,0 +1,14 @@ +{ + "suppressions": [ + { + "file": "src/index.ts", + "scope": ".exampleObject2.exampleObjectMethod", + "rule": "@typescript-eslint/no-inferrable-types" + }, + { + "file": "src/index.ts", + "scope": ".exampleObject2.exampleObjectProperty", + "rule": "@typescript-eslint/no-empty-function" + } + ] +} diff --git a/build-tests/eslint-bulk-suppressions-test/server/.eslintrc.js b/build-tests/eslint-bulk-suppressions-test/server/.eslintrc.js new file mode 100644 index 00000000000..7d32f3e4d32 --- /dev/null +++ b/build-tests/eslint-bulk-suppressions-test/server/.eslintrc.js @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('local-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution'); +// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 +require('local-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names'); +require('local-node-rig/profiles/default/includes/eslint/patch/eslint-bulk-suppressions'); + +module.exports = { + extends: [ + 'plugin:@typescript-eslint/recommended' + // 'local-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool', + // 'local-node-rig/profiles/default/includes/eslint/mixins/friendly-locals' + ], + ignorePatterns: ['.eslintrc.js'], + + overrides: [ + /** + * Override the parser from @rushstack/eslint-config. Since the config is coming + * from the workspace instead of the external NPM package, the versions of ESLint + * and TypeScript that the config consumes will be resolved from the devDependencies + * of the config instead of from the eslint-8-test package. Overriding the parser + * ensures that the these dependencies come from the eslint-8-test package. See: + * https://github.com/microsoft/rushstack/issues/3021 + */ + { + files: ['**/*.ts', '**/*.tsx'], + parser: '@typescript-eslint/parser', + parserOptions: { project: '../tsconfig.json', tsconfigRootDir: __dirname } + } + ] +}; diff --git a/build-tests/eslint-bulk-suppressions-test/server/src/index.ts b/build-tests/eslint-bulk-suppressions-test/server/src/index.ts new file mode 100644 index 00000000000..5e549720892 --- /dev/null +++ b/build-tests/eslint-bulk-suppressions-test/server/src/index.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// /* Object property and method code samples */ +export const exampleObject2 = { + // scopeId: '.exampleObject2.exampleObjectProperty + exampleObjectProperty: () => {}, + + exampleObjectMethod() { + // scopeId: '.exampleObject2.exampleObjectMethod' + const exampleUndefined: undefined = undefined; + return exampleUndefined; + } +}; + +/* Absurd examples */ +// export class AbsurdClass { +// absurdClassMethod() { +// return class AbsurdClass2 { +// absurdClassProperty; +// constructor() { +// const absurdObject = { +// // scopeId: '.AbsurdClass.absurdClassMethod.AbsurdClass2.constructor.absurdObject.absurdObjectMethod' +// absurdObjectMethod() {} +// }; +// this.absurdClassProperty = absurdObject; +// } +// }; +// } +// } + +// /* Type, interface, enum code samples */ +// export type ExampleObjectType = { +// // scopeId: '.ExampleObjectType' +// examplePropertyType: String; +// }; + +// // scopeId: '.ExampleInterface' +// export interface ExampleInterface {} + +// export enum ExampleEnum { +// A = 0, + +// B = 1, + +// C = 'exampleStringValue'['length'], + +// D = 1 +// } + +// /* Namespace, declare, module code samples */ +// // scopeId: '.ExampleModule' +// export namespace ExampleModule { +// // scopeId: '.ExampleModule.ExampleInterface2' +// export interface ExampleInterface2 {} +// } diff --git a/build-tests/eslint-bulk-suppressions-test/src/test-lab.ts b/build-tests/eslint-bulk-suppressions-test/src/test-lab.ts deleted file mode 100644 index 31023db24c9..00000000000 --- a/build-tests/eslint-bulk-suppressions-test/src/test-lab.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* Top-level scope code samples */ -// scope: '.', target: '.' -let exampleString: string = 5 + ''; - -const exampleObject = { - exampleString: exampleString -}; - -/* Function scope code samples */ -export function exampleFunction() { - const {}: Object = exampleObject; - - // scope: '.FunctionDeclaration', target: '.exampleFunction' - !!!exampleString as Boolean; -} - -// scope: '.ArrowFunctionExpression', target: '.x' -export const x = () => {}, - // scope: '.ArrowFunctionExpression', target: '.y' - y = () => {}, - // scope: '.ArrowFunctionExpression', target: '.z' - z = () => {}; - -/* Class scope code samples */ -export class ExampleClass { - // scope: '.ClassDeclaration', target: '.ExampleClass' - exampleClassProperty: String = exampleString + '4'; - - exampleMethod() { - // scope: '.ClassDeclaration.MethodDefinition', target: '.exampleClass.exampleMethod' - var exampleVar; - return exampleVar; - } -} - -/* Variable and anonymous constructs code samples */ -export const exampleArrowFunction = () => { - const exampleBoolean = true; - if (exampleBoolean) { - } - - exampleObject['exampleString']; -}; - -export const exampleAnonymousClass = class { - exampleClassProperty = 'x' + 'y'; - - // scope: '.ClassExpression.MethodDefinition', target: '.exampleAnonymousClass.constructor' - constructor() {} - - set exampleSetGet(val: string) { - // scope: '.ClassExpression.MethodDefinition', target: '.exampleAnonymousClass.exampleSetGet' - let exampleVariable: Number = 1; - this.exampleClassProperty = val + exampleVariable; - } - - get exampleSetGet() { - // scope: '.ClassExpression.MethodDefinition', target: '.exampleAnonymousClass.exampleSetGet' - return this.exampleClassProperty as String as string; - } -}; - -/* Object property and method code samples */ -export const exampleObject2 = { - // scope: '.ObjectExpression.ArrowFunctionExpression', target: '.exampleObject2.exampleObjectProperty - exampleObjectProperty: () => {}, - - exampleObjectMethod() { - // scope: '.ObjectExpression.MethodDefinition', target: '.exampleObject2.exampleObjectMethod' - const exampleUndefined: undefined = undefined; - return exampleUndefined; - } -}; - -/* Absurd examples */ -export class AbsurdClass { - absurdClassMethod() { - return class AbsurdClass2 { - absurdClassProperty; - constructor() { - const absurdObject = { - // scope: '.ClassDeclaration.MethodDefinition.ClassExpression.MethodDefinition.VariableDeclarator.Property' - // target: '.AbsurdClass.absurdClassMethod.AbsurdClass2.constructor.absurdObject.absurdObjectMethod' - absurdObjectMethod() {} - }; - this.absurdClassProperty = absurdObject; - } - }; - } -} - -/* Type, interface, enum code samples */ -export type ExampleObjectType = { - // scope: '.TSTypeAliasDeclaration', target: '.ExampleObjectType' - examplePropertyType: String; -}; - -// scope: '.TSInterfaceDeclaration', target: '.ExampleInterface' -export interface ExampleInterface {} - -export enum ExampleEnum { - A = 0, - - B = 1, - - C = 'exampleStringValue'['length'], - - D = 1 -} - -/* Namespace, declare, module code samples */ -// scope: '.TSModuleDeclaration', target: '.ExampleModule' -export namespace ExampleModule { - // scope: '.TSModuleDeclaration.TSInterfaceDeclaration', target: '.ExampleModule.ExampleInterface2' - export interface ExampleInterface2 {} -} diff --git a/build-tests/eslint-bulk-suppressions-test/tsconfig.json b/build-tests/eslint-bulk-suppressions-test/tsconfig.json index f84ddb77c24..2aa28fd2e64 100644 --- a/build-tests/eslint-bulk-suppressions-test/tsconfig.json +++ b/build-tests/eslint-bulk-suppressions-test/tsconfig.json @@ -19,6 +19,6 @@ "target": "es5", "lib": ["es5"] }, - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": ["client/**/*.ts", "client/**/*.tsx", "server/**/*.ts", "server/**/*.tsx"], "exclude": ["node_modules", "lib"] } diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 035af26a2c4..3d81a225bff 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -70,6 +70,10 @@ "name": "axios", "allowedCategories": [ "libraries" ] }, + { + "name": "commander", + "allowedCategories": [ "libraries" ] + }, { "name": "dependency-path", "allowedCategories": [ "libraries" ] diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 29fe10cfc76..29b36cceca8 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -2153,6 +2153,25 @@ importers: specifier: ~5.0.4 version: 5.0.4 + ../../eslint/eslint-bulk: + dependencies: + commander: + specifier: ~11.1.0 + version: 11.1.0 + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/heft-node-rig': + specifier: workspace:* + version: link:../../rigs/heft-node-rig + '@types/node': + specifier: 18.17.15 + version: 18.17.15 + eslint: + specifier: ~8.7.0 + version: 8.7.0 + ../../eslint/eslint-config: dependencies: '@rushstack/eslint-patch': @@ -14002,8 +14021,8 @@ packages: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} dev: true - /commander@11.0.0: - resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} dev: false @@ -21803,7 +21822,7 @@ packages: /pseudolocale@1.1.0: resolution: {integrity: sha512-OZ8I/hwYEJ3beN3IEcNnt8EpcqblH0/x23hulKBXjs+WhTTEle+ijCHCkh2bd+cIIeCuCwSCbBe93IthGG6hLw==} dependencies: - commander: 11.0.0 + commander: 11.1.0 dev: false /psl@1.9.0: diff --git a/eslint/eslint-bulk/config/rig.json b/eslint/eslint-bulk/config/rig.json new file mode 100755 index 00000000000..6ac88a96368 --- /dev/null +++ b/eslint/eslint-bulk/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "@rushstack/heft-node-rig" +} diff --git a/eslint/eslint-bulk/package.json b/eslint/eslint-bulk/package.json new file mode 100755 index 00000000000..c4491ba5d4b --- /dev/null +++ b/eslint/eslint-bulk/package.json @@ -0,0 +1,40 @@ +{ + "name": "@rushstack/eslint-bulk", + "version": "1.5.1", + "description": "A set of helper CLIs to use with the rushstack ESLint toolchain", + "main": "lib/index.js", + "license": "MIT", + "repository": { + "url": "https://github.com/microsoft/rushstack.git", + "type": "git", + "directory": "eslint/eslint-bulk" + }, + "homepage": "https://rushstack.io", + "bin": { + "eslint-bulk": "lib/index.js" + }, + "scripts": { + "build": "heft build --clean && chmod -R +x .", + "_phase:build": "heft run --only build -- --clean", + "eslint": "eslint" + }, + "keywords": [ + "eslintrc", + "eslint", + "bulk", + "legacy", + "retroactive", + "disable", + "ignore", + "suppression" + ], + "devDependencies": { + "@rushstack/heft": "workspace:*", + "@rushstack/heft-node-rig": "workspace:*", + "@types/node": "18.17.15", + "eslint": "~8.7.0" + }, + "dependencies": { + "commander": "~11.1.0" + } +} diff --git a/eslint/eslint-bulk/src/cleanup.ts b/eslint/eslint-bulk/src/cleanup.ts new file mode 100755 index 00000000000..613f497eaef --- /dev/null +++ b/eslint/eslint-bulk/src/cleanup.ts @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import type { ExecException } from 'child_process'; +import { exec } from 'child_process'; +import { Command } from 'commander'; +import { whichEslint } from './utils/which-eslint'; + +export function makeCleanupCommand(): Command { + const cleanup = new Command('cleanup'); + cleanup.argument('').action((files: string[]) => { + const eslintCLI = whichEslint(); + + const env = Object.assign({}, process.env); + Object.assign(env, { CLEANUP_ESLINT_BULK_SUPPRESSIONS: 'true' }); + + exec( + `${eslintCLI} ${files.join(' ')}`, + { env }, + (error: ExecException | null, stdout: string, stderr: string) => { + // if errorCount != 0, ESLint will process.exit(1) giving the false impression + // that the exec failed, even though linting errors are to be expected + const eslintOutputWithErrorRegex = /"errorCount":(?!0)\d+/; + const isEslintError = error !== null && error.code === 1 && eslintOutputWithErrorRegex.test(stdout); + + if (error && !isEslintError) { + console.error(`Execution error: ${error.message}`); + process.exit(1); + } + + if (stderr) { + console.error(`ESLint errors: ${stderr}`); + process.exit(1); + } + + console.log('Successfully cleaned up bulk suppressions'); + } + ); + }); + + return cleanup; +} diff --git a/eslint/eslint-bulk/src/index.ts b/eslint/eslint-bulk/src/index.ts new file mode 100755 index 00000000000..b6679f6f498 --- /dev/null +++ b/eslint/eslint-bulk/src/index.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import { Command } from 'commander'; +import { makeSuppressCommand } from './suppress'; +import { makeCleanupCommand } from './cleanup'; + +const program = new Command(); + +program.addCommand(makeSuppressCommand()); + +program.addCommand(makeCleanupCommand()); + +program.parse(process.argv); diff --git a/eslint/eslint-bulk/src/suppress.ts b/eslint/eslint-bulk/src/suppress.ts new file mode 100755 index 00000000000..e1d01d78c49 --- /dev/null +++ b/eslint/eslint-bulk/src/suppress.ts @@ -0,0 +1,57 @@ +#!/usr/bin/env node +import type { ExecException } from 'child_process'; +import { exec } from 'child_process'; +import { Command } from 'commander'; +import { whichEslint } from './utils/which-eslint'; + +export function makeSuppressCommand(): Command { + const suppress = new Command('suppress'); + suppress + .argument('') + .option('-R, --rule ') + .option('-A, --all') + .action((files: string[], options: { all: boolean; rule: string[] }) => { + if (!(options.all || options.rule)) { + throw new Error('Please specify at least one rule to suppress'); + } + + const eslintCLI = whichEslint(); + + const env = Object.assign({}, process.env); + if (options.all) { + Object.assign(env, { ESLINT_BULK_SUPPRESS_RULES: '*' }); + } else if (options.rule) { + Object.assign(env, { ESLINT_BULK_SUPPRESS_RULES: options.rule.join(',') }); + } + + exec( + `${eslintCLI} ${files.join(' ')} --format=json`, + { env }, + (error: ExecException | null, stdout: string, stderr: string) => { + // if errorCount != 0, ESLint will process.exit(1) giving the false impression + // that the exec failed, even though linting errors are to be expected + const eslintOutputWithErrorRegex = /"errorCount":(?!0)\d+/; + const isEslintError = error !== null && error.code === 1 && eslintOutputWithErrorRegex.test(stdout); + + if (error && !isEslintError) { + console.error(`Execution error: ${error.message}`); + process.exit(1); + } + + if (stderr) { + console.error(`ESLint errors: ${stderr}`); + process.exit(1); + } + + if (options.all) { + console.log(`Successfully suppressed all rules for file(s) ${files.join(',')}`); + } else if (options.rule) { + console.log( + `Successfully suppressed rules ${options.rule.join(',')} for file(s) ${files.join(',')}` + ); + } + } + ); + }); + return suppress; +} diff --git a/eslint/eslint-bulk/src/utils/which-eslint.ts b/eslint/eslint-bulk/src/utils/which-eslint.ts new file mode 100644 index 00000000000..fef10bd0586 --- /dev/null +++ b/eslint/eslint-bulk/src/utils/which-eslint.ts @@ -0,0 +1,30 @@ +import { execSync } from 'child_process'; +import path from 'path'; + +const SUPPORTED_VERSIONS = ['v8.6.0', 'v8.7.0', 'v8.21.0', 'v8.22.0', 'v8.23.0', 'v8.23.1']; + +export function whichEslint(): string { + // Try to find a local ESLint installation + try { + const localEslintPath = path.dirname(require.resolve('eslint', { paths: [__dirname, process.cwd()] })); + const eslintPackageJson = require(path.join(path.dirname(localEslintPath), 'package.json')); + const localEslintVersion = eslintPackageJson.version; + + if (SUPPORTED_VERSIONS.includes(localEslintVersion)) + return `node ${path.join(localEslintPath, 'bin', 'eslint.js')}`; + } catch { + // If we can't find a local ESLint or read its version, we do nothing and move on to the next step + } + + // Try to find a npx ESLint installation + try { + const npxEslintVersion = execSync('npx eslint -v', { stdio: 'ignore' }).toString().trim(); + + if (SUPPORTED_VERSIONS.includes(npxEslintVersion)) return 'npx eslint'; + } catch { + // If we can't find a npx ESLint or read its version, we do nothing and move on to the next step + } + + // If we haven't returned by now, we didn't find a supported local or npx version, so we fetch a specific remote version + return 'npx eslint@8.23.1'; +} diff --git a/eslint/eslint-bulk/tsconfig.json b/eslint/eslint-bulk/tsconfig.json new file mode 100755 index 00000000000..a114c3448ed --- /dev/null +++ b/eslint/eslint-bulk/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/eslint/eslint-config/mixins/bulk-suppressions.js b/eslint/eslint-config/mixins/bulk-suppressions.js deleted file mode 100644 index a4303dca9d6..00000000000 --- a/eslint/eslint-config/mixins/bulk-suppressions.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - plugins: [ - // Plugin documentation: https://www.npmjs.com/package/@rushstack/eslint-plugin - '@rushstack/eslint-plugin' - ], - rules: { - '@rushstack/is-bulk-suppressed': 'warn' - } -}; diff --git a/eslint/eslint-patch/src/eslint-bulk-suppressions/bulk-suppressions-patch.ts b/eslint/eslint-patch/src/eslint-bulk-suppressions/bulk-suppressions-patch.ts index b64667623f4..bd98e337436 100644 --- a/eslint/eslint-patch/src/eslint-bulk-suppressions/bulk-suppressions-patch.ts +++ b/eslint/eslint-patch/src/eslint-bulk-suppressions/bulk-suppressions-patch.ts @@ -46,38 +46,16 @@ function getNodeName(node: TSESTree.Node): string | null { return null; } -// function getScopeAncestry(scope: Scope | null): Scope[] { -// if (scope === null || scope.block.type === 'Program') return []; - -// return [...getScopeAncestry(scope.upper), scope]; -// } - -// export function serializeNodeScope( -// context: Readonly>, -// node: TSESTree.Node -// ): string { -// const scopeManager = context.getSourceCode().scopeManager; -// if (!scopeManager) throw new Error('scopeManager is null'); - -// const scopeAncestry = getScopeAncestry(scopeManager.acquire(node, true)); - -// const scopeAncestryNames = scopeAncestry.map((scope) => getNodeName(scope.block)).filter(Boolean); - -// return '.' + scopeAncestryNames.join('.'); -// } - -function calculateScope(node: any | (TSESTree.Node & { parent?: TSESTree.Node }) | undefined): { - scope: string; -} { +function calculateScope(node: any | (TSESTree.Node & { parent?: TSESTree.Node }) | undefined): string { const scopes = []; for (let current = node; current; current = current.parent) { const scopeForASTNode = getNodeName(current); if (scopeForASTNode !== null) scopes.unshift(scopeForASTNode); } - if (scopes.length === 0) return { scope: '.' }; + if (scopes.length === 0) return '.'; - return { scope: '.' + scopes.join('.') }; + return '.' + scopes.join('.'); } /** @@ -100,29 +78,47 @@ function findEslintrcDirectory(fileAbsolutePath: string): string { } function validateSuppressionsJson(json: BulkSuppressionsJson): json is BulkSuppressionsJson { - if (typeof json !== 'object') return false; - if (json === null) return false; - if (!json.hasOwnProperty('suppressions')) return false; - if (!Array.isArray(json.suppressions)) return false; + if (typeof json !== 'object') throw new Error(`Invalid JSON object: ${JSON.stringify(json, null, 2)}`); + if (json === null) throw new Error('JSON object is null.'); + if (!json.hasOwnProperty('suppressions')) throw new Error('Missing "suppressions" property.'); + if (!Array.isArray(json.suppressions)) throw new Error('"suppressions" property is not an array.'); if ( !json.suppressions.every((suppression) => { - if (typeof suppression !== 'object') return false; - if (suppression === null) return false; - if (!suppression.hasOwnProperty('file')) return false; - if (typeof suppression.file !== 'string') return false; - if (!suppression.hasOwnProperty('scope')) return false; - if (typeof suppression.scope !== 'string') return false; - if (!suppression.hasOwnProperty('rule')) return false; - if (typeof suppression.rule !== 'string') return false; + if (typeof suppression !== 'object') + throw new Error(`Invalid suppression: ${JSON.stringify(suppression, null, 2)}`); + if (suppression === null) + throw new Error(`Suppression is null: ${JSON.stringify(suppression, null, 2)}`); + if (!suppression.hasOwnProperty('file')) + throw new Error(`Missing "file" property in suppression: ${JSON.stringify(suppression, null, 2)}`); + if (typeof suppression.file !== 'string') + throw new Error( + `"file" property in suppression is not a string: ${JSON.stringify(suppression, null, 2)}` + ); + if (!suppression.hasOwnProperty('scope')) + throw new Error(`Missing "scope" property in suppression: ${JSON.stringify(suppression, null, 2)}`); + if (typeof suppression.scope !== 'string') + throw new Error( + `"scope" property in suppression is not a string: ${JSON.stringify(suppression, null, 2)}` + ); + if (!suppression.hasOwnProperty('rule')) + throw new Error(`Missing "rule" property in suppression: ${JSON.stringify(suppression, null, 2)}`); + if (typeof suppression.rule !== 'string') + throw new Error( + `"rule" property in suppression is not a string: ${JSON.stringify(suppression, null, 2)}` + ); return true; }) - ) - return false; + ) { + throw new Error( + `Invalid suppression in "suppressions" array: ${JSON.stringify(json.suppressions, null, 2)}` + ); + } return true; } -function readSuppressionsJson(eslintrcDirectory: string): BulkSuppressionsJson { +function readSuppressionsJson(fileAbsolutePath: string): BulkSuppressionsJson { + const eslintrcDirectory = findEslintrcDirectory(fileAbsolutePath); const suppressionsPath = path.join(eslintrcDirectory, '.eslint-bulk-suppressions.json'); let suppressionsJson = { suppressions: [] }; try { @@ -130,7 +126,8 @@ function readSuppressionsJson(eslintrcDirectory: string): BulkSuppressionsJson { suppressionsJson = JSON.parse(fileContent); if (!validateSuppressionsJson(suppressionsJson)) { - console.log( + console.log('invalid data', suppressionsJson); + console.warn( `Unexpected file content in .eslint-bulk-suppressions.json. JSON expected to be in the following format: { suppressions: { @@ -144,24 +141,34 @@ Please check file content, or delete file if suppressions are no longer needed. ); suppressionsJson = { suppressions: [] }; } - } catch (err) { - // Do nothing and let JSON5 log the error. suppressionsJson will stay as the initialized value. + } catch { + // Do nothing and let JSON.parse() log the error. suppressionsJson will stay as the initialized value. } return suppressionsJson; } -function serializeFileScopeRule(suppression: { file: string; scope: string; rule: string }): string { - return `${suppression.file}|${suppression.scope}|${suppression.rule}`; -} - -function shouldWriteSuppression(rule: string): boolean { +function shouldWriteSuppression(fileAbsolutePath: string, suppression: Suppression): boolean { if (process.env.ESLINT_BULK_SUPPRESS_RULES === undefined) return false; + if (isSuppressed(fileAbsolutePath, suppression)) return false; + const rulesToSuppress = process.env.ESLINT_BULK_SUPPRESS_RULES.split(','); if (rulesToSuppress.length === 1 && rulesToSuppress[0] === '*') return true; - return rulesToSuppress.includes(rule); + return rulesToSuppress.includes(suppression.rule); +} + +function isSuppressed(fileAbsolutePath: string, suppression: Suppression): boolean { + const suppressionsJson = readSuppressionsJson(fileAbsolutePath); + return ( + suppressionsJson.suppressions.find( + (element) => + element.file === suppression.file && + element.scope === suppression.scope && + element.rule === suppression.rule + ) !== undefined + ); } function insort(array: T[], item: T, compareFunction: (a: T, b: T) => number): void { @@ -180,26 +187,32 @@ function compareSuppressions(a: Suppression, b: Suppression): -1 | 0 | 1 { return 0; } -function writeSuppression(params: { - eslintrcDirectory: string; - file: string; - scope: string; - rule: string; -}): void { - const { eslintrcDirectory, file, scope, rule } = params; - const suppressionsJson = readSuppressionsJson(eslintrcDirectory); +function writeSuppressionToFile( + fileAbsolutePath: string, + suppression: { + file: string; + scope: string; + rule: string; + } +): void { + const eslintrcDirectory = findEslintrcDirectory(fileAbsolutePath); + const suppressionsJson = readSuppressionsJson(fileAbsolutePath); - insort(suppressionsJson.suppressions, { file, scope, rule }, compareSuppressions); + insort(suppressionsJson.suppressions, suppression, compareSuppressions); const suppressionsPath = path.join(eslintrcDirectory, '.eslint-bulk-suppressions.json'); fs.writeFileSync(suppressionsPath, JSON.stringify(suppressionsJson, null, 2)); } -function readSerializedSuppressionsSet(fileAbsolutePath: string) { - const eslintrcDirectory = findEslintrcDirectory(fileAbsolutePath); - const suppressionsJson = readSuppressionsJson(eslintrcDirectory); - const serializedSuppressionsSet = new Set(suppressionsJson.suppressions.map(serializeFileScopeRule)); - return serializedSuppressionsSet; +const usedSuppressions = new Set(); + +function serializeSuppression(fileAbsolutePath: string, suppression: Suppression): string { + return `${fileAbsolutePath}|${suppression.file}|${suppression.scope}|${suppression.rule}`; +} + +function deserializeSuppression(serializedSuppression: string): Suppression { + const [file, scope, rule] = serializedSuppression.split('|'); + return { file, scope, rule }; } // One-line insert into the ruleContext report method to prematurely exit if the ESLint problem has been suppressed @@ -214,25 +227,42 @@ export function shouldBulkSuppress(params: { const { filename: fileAbsolutePath, currentNode, ruleId: rule } = params; const eslintrcDirectory = findEslintrcDirectory(fileAbsolutePath); const fileRelativePath = path.relative(eslintrcDirectory, fileAbsolutePath); - const { scope } = calculateScope(currentNode); - const serializedFileScopeRule = serializeFileScopeRule({ - file: fileRelativePath, - scope, - rule - }); + const scope = calculateScope(currentNode); + const suppression = { file: fileRelativePath, scope, rule }; - if ( - shouldWriteSuppression(rule) && - !readSerializedSuppressionsSet(fileAbsolutePath).has(serializedFileScopeRule) - ) - writeSuppression({ eslintrcDirectory, file: fileRelativePath, scope, rule }); + if (shouldWriteSuppression(fileAbsolutePath, suppression)) { + writeSuppressionToFile(fileAbsolutePath, suppression); + } - const shouldBulkSuppress: boolean = - readSerializedSuppressionsSet(fileAbsolutePath).has(serializedFileScopeRule); + const shouldBulkSuppress = isSuppressed(fileAbsolutePath, suppression); + + if (shouldBulkSuppress) { + usedSuppressions.add(serializeSuppression(fileAbsolutePath, suppression)); + } return shouldBulkSuppress; } +export function BulkSuppressionsCleanUp(params: { filename: string }): void { + if (process.env.CLEANUP_ESLINT_BULK_SUPPRESSIONS !== 'true') return; + + console.log(usedSuppressions); + const { filename: fileAbsolutePath } = params; + const suppressionsJson = readSuppressionsJson(fileAbsolutePath); + const newSuppressionsJson = { + suppressions: suppressionsJson.suppressions.filter((suppression) => { + // console.log( + // serializeSuppression(fileAbsolutePath, suppression), + // usedSuppressions.has(serializeSuppression(fileAbsolutePath, suppression)) + // ); + return usedSuppressions.has(serializeSuppression(fileAbsolutePath, suppression)); + }) + }; + const eslintrcDirectory = findEslintrcDirectory(fileAbsolutePath); + const suppressionsPath = path.join(eslintrcDirectory, '.eslint-bulk-suppressions.json'); + fs.writeFileSync(suppressionsPath, JSON.stringify(newSuppressionsJson, null, 2)); +} + // utility function for linter-patch.js to make require statements that use relative paths in linter.js work in linter-patch.js export function requireFromPathToLinterJS(importPath: string): any { if (!eslintFolder) return require(importPath); @@ -281,8 +311,3 @@ export function patchClass(originalClass: new () => T, patchedCl } } } - -// module.exports = { -// requireFromPathToLinterJS, -// shouldBulkSuppress -// }; diff --git a/eslint/eslint-patch/src/eslint-bulk-suppressions/index.ts b/eslint/eslint-patch/src/eslint-bulk-suppressions/index.ts index 5a8da9df277..117a4076da1 100644 --- a/eslint/eslint-patch/src/eslint-bulk-suppressions/index.ts +++ b/eslint/eslint-patch/src/eslint-bulk-suppressions/index.ts @@ -5,6 +5,7 @@ import { eslintFolder } from '../_patch-base'; import { patchClass, whichPatchToLoad } from './bulk-suppressions-patch'; if (!eslintFolder) process.exit(); + const patchForSpecificESLintVersion = whichPatchToLoad(eslintFolder); if (!patchForSpecificESLintVersion) process.exit(); const { Linter: LinterPatch } = require(`./${patchForSpecificESLintVersion}`); diff --git a/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.21.0-to-v8.23.1.js b/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.21.0-to-v8.23.1.js index 3be3685beaa..bc5f0d73202 100644 --- a/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.21.0-to-v8.23.1.js +++ b/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.21.0-to-v8.23.1.js @@ -1184,6 +1184,10 @@ function runRules( } }); + // --- BEGIN MONKEY PATCH --- + bulkSuppressionsPatch.BulkSuppressionsCleanUp({ filename }); + // --- END MONKEY PATCH --- + return lintingProblems; } diff --git a/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.6.0-to-v8.7.0.js b/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.6.0-to-v8.7.0.js index 1f978f901ad..951888bb60e 100644 --- a/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.6.0-to-v8.7.0.js +++ b/eslint/eslint-patch/src/eslint-bulk-suppressions/linter-patch-for-eslint-v8.6.0-to-v8.7.0.js @@ -1146,6 +1146,10 @@ function runRules( } }); + // --- BEGIN MONKEY PATCH --- + bulkSuppressionsPatch.BulkSuppressionsCleanUp({ filename }); + // --- END MONKEY PATCH --- + return lintingProblems; } diff --git a/rigs/local-node-rig/profiles/default/includes/eslint/mixins/bulk-suppressions.js b/rigs/local-node-rig/profiles/default/includes/eslint/mixins/bulk-suppressions.js deleted file mode 100644 index a4303dca9d6..00000000000 --- a/rigs/local-node-rig/profiles/default/includes/eslint/mixins/bulk-suppressions.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - plugins: [ - // Plugin documentation: https://www.npmjs.com/package/@rushstack/eslint-plugin - '@rushstack/eslint-plugin' - ], - rules: { - '@rushstack/is-bulk-suppressed': 'warn' - } -}; diff --git a/rush.json b/rush.json index 725d57f8fb9..dc99fe693d2 100644 --- a/rush.json +++ b/rush.json @@ -657,6 +657,12 @@ }, // "eslint" folder (alphabetical order) + { + "packageName": "@rushstack/eslint-bulk", + "projectFolder": "eslint/eslint-bulk", + "reviewCategory": "libraries", + "shouldPublish": true + }, { "packageName": "@rushstack/eslint-config", "projectFolder": "eslint/eslint-config",