chore: enable a bunch more lint rules (#14794)

This commit is contained in:
Simen Bekkhus 2023-12-29 08:49:03 +01:00 committed by GitHub
parent 175665ec49
commit 0469b4c698
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 144 additions and 115 deletions

View File

@ -35,13 +35,19 @@ module.exports = {
'plugin:eslint-comments/recommended',
'plugin:prettier/recommended',
'plugin:unicorn/recommended',
'plugin:promise/recommended',
],
globals: {
console: 'readonly',
},
overrides: [
{
extends: ['plugin:@typescript-eslint/strict', 'plugin:import/typescript'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/strict',
'plugin:@typescript-eslint/stylistic',
'plugin:import/typescript',
],
files: ['*.ts', '*.tsx'],
plugins: ['@typescript-eslint/eslint-plugin', 'local'],
rules: {
@ -59,6 +65,7 @@ module.exports = {
],
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
// TS verifies these
'consistent-return': 'off',
'no-dupe-class-members': 'off',
@ -68,10 +75,7 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-invalid-void-type': 'off',
// TODO: part of "stylistic" rules, remove explicit activation when that lands
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/consistent-type-definitions': 'off',
// not needed to be enforced for TS
'import/namespace': 'off',
@ -326,6 +330,7 @@ module.exports = {
rules: {
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/class-literal-property-style': 'off',
},
},
{
@ -427,6 +432,12 @@ module.exports = {
'unicorn/no-static-only-class': 'off',
},
},
{
files: '**/*.mjs',
rules: {
'unicorn/prefer-top-level-await': 'error',
},
},
],
parser: '@typescript-eslint/parser',
parserOptions: {
@ -617,6 +628,11 @@ module.exports = {
'prefer-arrow-callback': ['error', {allowNamedFunctions: true}],
'prefer-const': 'error',
'prefer-template': 'error',
'promise/always-return': 'off',
'promise/catch-or-return': 'off',
'promise/no-callback-in-promise': 'off',
quotes: [
'error',
'single',
@ -655,10 +671,8 @@ module.exports = {
'unicorn/prefer-event-target': 'off',
'unicorn/prefer-switch': 'off',
'unicorn/prefer-ternary': 'off',
'unicorn/switch-case-braces': 'off',
// TODO: enable for `.mjs` files
'unicorn/prefer-top-level-await': 'off',
'unicorn/switch-case-braces': 'off',
// TODO: decide whether or not we want these
'unicorn/filename-case': 'off',

View File

@ -8,7 +8,7 @@
'use strict';
it.concurrent('Good Test', async () => {
await new Promise(r => setTimeout(r, 100));
await new Promise(resolve => setTimeout(resolve, 100));
});
it.concurrent('Bad Test', async () => {

View File

@ -7,7 +7,7 @@
*/
'use strict';
const sleep = ms => new Promise(r => setTimeout(r, ms));
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
describe('A', () => {
it.concurrent('a', async () => {

View File

@ -41,6 +41,7 @@
"eslint-plugin-local": "link:./.eslintplugin",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-unicorn": "^50.0.0",
"execa": "^5.0.0",
"find-process": "^1.4.1",

View File

@ -133,7 +133,7 @@ FUNCTIONS.mock = args => {
);
}
const ids: Set<NodePath<Identifier>> = new Set();
const ids = new Set<NodePath<Identifier>>();
const parentScope = moduleFactory.parentPath.scope;
// @ts-expect-error: ReferencedIdentifier and denylist are not known on visitors
moduleFactory.traverse(IDVisitor, {ids});

View File

@ -76,13 +76,8 @@ function eq(
}
const testerContext: TesterContext = {equals};
for (let i = 0; i < customTesters.length; i++) {
const customTesterResult = customTesters[i].call(
testerContext,
a,
b,
customTesters,
);
for (const item of customTesters) {
const customTesterResult = item.call(testerContext, a, b, customTesters);
if (customTesterResult !== undefined) {
return customTesterResult;
}

View File

@ -203,7 +203,7 @@ const makeResolveMatcher =
)}\n\n` +
'Received promise rejected instead of resolved\n' +
`Rejected to value: ${matcherUtils.printReceived(reason)}`;
return Promise.reject(outerErr);
throw outerErr;
},
);
};
@ -254,7 +254,7 @@ const makeRejectMatcher =
)}\n\n` +
'Received promise resolved instead of rejected\n' +
`Resolved to value: ${matcherUtils.printReceived(result)}`;
return Promise.reject(outerErr);
throw outerErr;
},
reason =>
makeThrowingMatcher(matcher, isNot, 'rejects', reason, innerErr).apply(

View File

@ -98,11 +98,10 @@ export interface BaseExpect {
setState(state: Partial<MatcherState>): void;
}
export type Expect = {
<T = unknown>(
export type Expect = (<T = unknown>(
actual: T,
): Matchers<void, T> & Inverse<Matchers<void, T>> & PromiseMatchers<T>;
} & BaseExpect &
) => Matchers<void, T> & Inverse<Matchers<void, T>> & PromiseMatchers<T>) &
BaseExpect &
AsymmetricMatchers &
Inverse<Omit<AsymmetricMatchers, 'any' | 'anything'>>;

View File

@ -113,8 +113,8 @@ describe('collectHandles', () => {
const server = http.createServer((_, response) => response.end('ok'));
// Start and stop server.
await new Promise(r => server.listen(0, r));
await new Promise(r => server.close(r));
await new Promise(resolve => server.listen(0, resolve));
await new Promise(resolve => server.close(resolve));
const openHandles = await handleCollector();
expect(openHandles).toHaveLength(0);
@ -130,11 +130,13 @@ describe('collectHandles', () => {
// creates a long-lived `TCPSERVERWRAP` resource. We want to make sure we
// capture that long-lived resource.
const server = new http.Server();
await new Promise(r => server.listen({host: 'localhost', port: 0}, r));
await new Promise(resolve =>
server.listen({host: 'localhost', port: 0}, resolve),
);
const openHandles = await handleCollector();
await new Promise(r => server.close(r));
await new Promise(resolve => server.close(resolve));
expect(openHandles).toContainEqual(
expect.objectContaining({message: 'TCPSERVERWRAP'}),

View File

@ -88,7 +88,7 @@ const regularUpdateGlobalConfig = require('../lib/updateGlobalConfig').default;
const updateGlobalConfig = jest.fn(regularUpdateGlobalConfig);
jest.doMock('../lib/updateGlobalConfig', () => updateGlobalConfig);
const nextTick = () => new Promise(res => process.nextTick(res));
const nextTick = () => new Promise(resolve => process.nextTick(resolve));
beforeAll(() => {
jest.spyOn(process, 'on').mockImplementation(() => {});
@ -771,7 +771,9 @@ describe('Watch mode flows', () => {
it('prevents Jest from handling keys when active and returns control when end is called', async () => {
let resolveShowPrompt;
const run = jest.fn(() => new Promise(res => (resolveShowPrompt = res)));
const run = jest.fn(
() => new Promise(resolve => (resolveShowPrompt = resolve)),
);
const pluginPath = `${__dirname}/__fixtures__/plugin_path_1`;
jest.doMock(
pluginPath,

View File

@ -68,7 +68,7 @@ jest.doMock(
const watch = require('../watch').default;
const nextTick = () => new Promise(res => process.nextTick(res));
const nextTick = () => new Promise(resolve => process.nextTick(resolve));
const globalConfig = {
rootDir: '',

View File

@ -41,7 +41,7 @@ class TestNamePatternPlugin extends BaseWatchPlugin {
globalConfig: Config.GlobalConfig,
updateConfigAndRun: UpdateConfigCallback,
): Promise<void> {
return new Promise((res, rej) => {
return new Promise((resolve, reject) => {
const testNamePatternPrompt = new TestNamePatternPrompt(
this._stdout,
this._prompt,
@ -50,9 +50,9 @@ class TestNamePatternPlugin extends BaseWatchPlugin {
testNamePatternPrompt.run(
(value: string) => {
updateConfigAndRun({mode: 'watch', testNamePattern: value});
res();
resolve();
},
rej,
reject,
{
header: activeFilters(globalConfig),
},

View File

@ -41,7 +41,7 @@ class TestPathPatternPlugin extends BaseWatchPlugin {
globalConfig: Config.GlobalConfig,
updateConfigAndRun: UpdateConfigCallback,
): Promise<void> {
return new Promise((res, rej) => {
return new Promise((resolve, reject) => {
const testPathPatternPrompt = new TestPathPatternPrompt(
this._stdout,
this._prompt,
@ -50,9 +50,9 @@ class TestPathPatternPlugin extends BaseWatchPlugin {
testPathPatternPrompt.run(
(value: string) => {
updateConfigAndRun({mode: 'watch', testPathPatterns: [value]});
res();
resolve();
},
rej,
reject,
{
header: activeFilters(globalConfig),
},

View File

@ -67,7 +67,7 @@ class UpdateSnapshotInteractivePlugin extends BaseWatchPlugin {
updateConfigAndRun: Function,
): Promise<void> {
if (this._failedSnapshotTestAssertions.length > 0) {
return new Promise(res => {
return new Promise(resolve => {
this._snapshotInteractiveMode.run(
this._failedSnapshotTestAssertions,
(assertion, shouldUpdateSnapshot) => {
@ -79,7 +79,7 @@ class UpdateSnapshotInteractivePlugin extends BaseWatchPlugin {
updateSnapshot: shouldUpdateSnapshot ? 'all' : 'none',
});
if (!this._snapshotInteractiveMode.isActive()) {
res();
resolve();
}
},
);

View File

@ -27,8 +27,9 @@ export function extract(contents: string): string {
}
export function strip(contents: string): string {
const match = contents.match(docblockRe);
return match && match[0] ? contents.slice(match[0].length) : contents;
const matchResult = contents.match(docblockRe);
const match = matchResult?.[0];
return match == null ? contents : contents.slice(match.length);
}
export function parse(docblock: string): Pragmas {

View File

@ -15,7 +15,7 @@
//
// Feel free to add any extensions that cannot be a Haste module.
const extensions: Set<string> = new Set([
const extensions = new Set<string>([
// JSONs are never haste modules, except for "package.json", which is handled.
'.json',

View File

@ -71,9 +71,9 @@ class CallTracker {
};
this.allArgs = function () {
const callArgs = [];
for (let i = 0; i < calls.length; i++) {
callArgs.push(calls[i].args);
const callArgs: Array<unknown> = [];
for (const call of calls) {
callArgs.push(call.args);
}
return callArgs;

View File

@ -56,8 +56,7 @@ export default class ReportDispatcher implements Reporter {
constructor(methods: Array<keyof Reporter>) {
const dispatchedMethods = methods || [];
for (let i = 0; i < dispatchedMethods.length; i++) {
const method = dispatchedMethods[i];
for (const method of dispatchedMethods) {
this[method] = (function (m) {
return function () {
dispatch(m, arguments);
@ -86,8 +85,7 @@ export default class ReportDispatcher implements Reporter {
if (reporters.length === 0 && fallbackReporter !== null) {
reporters.push(fallbackReporter);
}
for (let i = 0; i < reporters.length; i++) {
const reporter = reporters[i];
for (const reporter of reporters) {
if (reporter[method]) {
// @ts-expect-error: wrong context
reporter[method].apply(reporter, args);

View File

@ -190,8 +190,7 @@ export default class Suite {
};
this.result.failedExpectations.push(expectationResultFactory(data));
} else {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
for (const child of this.children) {
child.onException.apply(child, args);
}
}
@ -205,8 +204,7 @@ export default class Suite {
throw new ExpectationFailed();
}
} else {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
for (const child of this.children) {
try {
child.addExpectationResult.apply(child, args);
} catch {

View File

@ -36,7 +36,7 @@ const isMap = (value: any): value is Map<unknown, unknown> =>
export default function deepCyclicCopyReplaceable<T>(
value: T,
cycles: WeakMap<any, any> = new WeakMap(),
cycles = new WeakMap<any, any>(),
): T {
if (typeof value !== 'object' || value === null) {
return value;

View File

@ -101,9 +101,7 @@ interface SomeFunctionObject {
one: {
(oneA: number, oneB?: boolean): boolean;
more: {
time: {
(time: number): void;
};
time: (time: number) => void;
};
};
}

View File

@ -30,7 +30,7 @@ export type MockMetadata<T, MetadataType = MockMetadataType> = {
length?: number;
};
export type ClassLike = {new (...args: any): any};
export type ClassLike = new (...args: any) => any;
export type FunctionLike = (...args: any) => any;
export type ConstructorLikeKeys<T> = keyof {
@ -91,7 +91,7 @@ export type MockedShallow<T> = T extends ClassLike
: T;
export type UnknownFunction = (...args: Array<unknown>) => unknown;
export type UnknownClass = {new (...args: Array<unknown>): unknown};
export type UnknownClass = new (...args: Array<unknown>) => unknown;
export type SpiedClass<T extends ClassLike = UnknownClass> = MockInstance<
(...args: ConstructorParameters<T>) => InstanceType<T>
@ -533,9 +533,7 @@ export class ModuleMocker {
) {
const ownNames = Object.getOwnPropertyNames(object);
for (let i = 0; i < ownNames.length; i++) {
const prop = ownNames[i];
for (const prop of ownNames) {
if (!isReadonlyProp(object, prop)) {
const propDesc = Object.getOwnPropertyDescriptor(object, prop);
if ((propDesc !== undefined && !propDesc.get) || object.__esModule) {

View File

@ -75,10 +75,10 @@ const isRecoverableError = (error: unknown) => {
if (jestProjectConfig.transform) {
let transformerPath = null;
for (let i = 0; i < jestProjectConfig.transform.length; i++) {
if (new RegExp(jestProjectConfig.transform[i][0]).test('foobar.js')) {
transformerPath = jestProjectConfig.transform[i][1];
transformerConfig = jestProjectConfig.transform[i][2];
for (const transform of jestProjectConfig.transform) {
if (new RegExp(transform[0]).test('foobar.js')) {
transformerPath = transform[1];
transformerConfig = transform[2];
break;
}
}

View File

@ -33,10 +33,11 @@ export async function run(
if (cliArgv) {
argv = cliArgv;
} else {
argv = <Config.Argv>(
yargs.usage(args.usage).help(false).version(false).options(args.options)
.argv
);
argv = yargs
.usage(args.usage)
.help(false)
.version(false)
.options(args.options).argv as Config.Argv;
validateCLIOptions(argv, {...args.options, deprecationEntries});
}

View File

@ -231,10 +231,8 @@ export default class GitHubActionsReporter extends BaseReporter {
});
} else {
let alreadyInserted = false;
for (let index = 0; index < branches.length; index++) {
if (
this.arrayEqual(branches[index], element.ancestorTitles.slice(0, 1))
) {
for (const branch of branches) {
if (this.arrayEqual(branch, element.ancestorTitles.slice(0, 1))) {
alreadyInserted = true;
break;
}
@ -286,10 +284,10 @@ export default class GitHubActionsReporter extends BaseReporter {
)
) {
let alreadyInserted = false;
for (let index = 0; index < branches.length; index++) {
for (const branch of branches) {
if (
this.arrayEqual(
branches[index],
branch,
element.ancestorTitles.slice(0, ancestors.length + 1),
)
) {

View File

@ -192,8 +192,8 @@ export default class Status {
let height = 0;
for (let i = 0; i < content.length; i++) {
if (content[i] === '\n') {
for (const char of content) {
if (char === '\n') {
height++;
}
}

View File

@ -139,7 +139,7 @@ export class DependencyResolver {
};
const relatedPaths = new Set<string>();
const changed: Set<string> = new Set();
const changed = new Set<string>();
for (const path of paths) {
if (this._hasteFS.exists(path)) {
const modulePath = isSnapshotPath(path)

View File

@ -102,7 +102,7 @@ export default class TestRunner extends EmittingTestRunner {
}
async #createParallelTestRun(tests: Array<Test>, watcher: TestWatcher) {
const resolvers: Map<string, SerializableResolver> = new Map();
const resolvers = new Map<string, SerializableResolver>();
for (const test of tests) {
if (!resolvers.has(test.context.config.id)) {
resolvers.set(test.context.config.id, {
@ -167,7 +167,7 @@ export default class TestRunner extends EmittingTestRunner {
return promise;
});
const onInterrupt = new Promise((_, reject) => {
const onInterrupt = new Promise((_resolve, reject) => {
watcher.on('change', state => {
if (state.interrupted) {
reject(new CancelRun());

View File

@ -254,8 +254,8 @@ class ScriptTransformer {
return undefined;
}
for (let i = 0; i < transformEntry.length; i++) {
const [transformRegExp, transformPath] = transformEntry[i];
for (const item of transformEntry) {
const [transformRegExp, transformPath] = item;
if (transformRegExp.test(filename)) {
return [transformRegExp.source, transformPath];
}
@ -1017,12 +1017,8 @@ const calcTransformRegExp = (config: Config.ProjectConfig) => {
}
const transformRegexp: Array<[RegExp, string, Record<string, unknown>]> = [];
for (let i = 0; i < config.transform.length; i++) {
transformRegexp.push([
new RegExp(config.transform[i][0]),
config.transform[i][1],
config.transform[i][2],
]);
for (const item of config.transform) {
transformRegexp.push([new RegExp(item[0]), item[1], item[2]]);
}
return transformRegexp;

View File

@ -113,9 +113,7 @@ interface Each<EachFn extends TestFn | BlockFn> {
) => void;
}
export interface HookBase {
(fn: HookFn, timeout?: number): void;
}
export type HookBase = (fn: HookFn, timeout?: number) => void;
export interface Failing<T extends TestFn> {
(testName: TestNameLike, fn: T, timeout?: number): void;

View File

@ -15,7 +15,7 @@ export type DeepCyclicCopyOptions = {
export default function deepCyclicCopy<T>(
value: T,
options: DeepCyclicCopyOptions = {blacklist: EMPTY, keepPrototype: false},
cycles: WeakMap<any, any> = new WeakMap(),
cycles = new WeakMap<any, any>(),
): T {
if (typeof value !== 'object' || value === null || Buffer.isBuffer(value)) {
return value;

View File

@ -63,8 +63,8 @@ export default function globsToMatcher(globs: Array<string>): Matcher {
let kept = undefined;
let negatives = 0;
for (let i = 0; i < matchers.length; i++) {
const {isMatch, negated} = matchers[i];
for (const matcher of matchers) {
const {isMatch, negated} = matcher;
if (negated) {
negatives++;

View File

@ -42,7 +42,7 @@ export function validationCondition(
export function multipleValidOptions<T extends Array<unknown>>(
...args: T
): T[number] {
const options = <T>[...args];
const options = [...args] as T;
// @ts-expect-error: no index signature
options[MULTIPLE_VALID_OPTIONS_SYMBOL] = true;

View File

@ -41,7 +41,7 @@ export default abstract class PatternPrompt {
this._pipe.write(ansiEscapes.cursorHide);
this._pipe.write(CLEAR);
if (options && options.header) {
if (typeof options?.header === 'string' && options.header) {
this._pipe.write(`${options.header}\n`);
this._currentUsageRows = usageRows + options.header.split('\n').length;
} else {

View File

@ -81,13 +81,11 @@ export interface WatchPlugin {
updateConfigAndRun: UpdateConfigCallback,
) => Promise<void | boolean>;
}
export interface WatchPluginClass {
new (options: {
export type WatchPluginClass = new (options: {
config: Record<string, unknown>;
stdin: ReadStream;
stdout: WriteStream;
}): WatchPlugin;
}
}) => WatchPlugin;
export type ScrollOptions = {
offset: number;

View File

@ -342,10 +342,10 @@ function printPlugin(
}
function findPlugin(plugins: Plugins, val: unknown) {
for (let p = 0; p < plugins.length; p++) {
for (const plugin of plugins) {
try {
if (plugins[p].test(val)) {
return plugins[p];
if (plugin.test(val)) {
return plugin;
}
} catch (error: any) {
throw new PrettyFormatPluginError(error.message, error.stack);

View File

@ -93,7 +93,10 @@ async function buildNodePackages() {
process.stdout.write(`${OK}\n`);
}
buildNodePackages().catch(error => {
try {
await buildNodePackages();
} catch (error) {
process.stderr.write(`${ERROR}\n`);
console.error(error);
process.exitCode = 1;
});
}

View File

@ -85,7 +85,10 @@ try {
fix,
fixTypes: ['problem', 'suggestion', 'layout'],
overrideConfig: {
extends: ['plugin:@typescript-eslint/recommended-type-checked'],
extends: [
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:@typescript-eslint/stylistic-type-checked',
],
overrides: [
{
files: ['**/__tests__/**'],
@ -97,6 +100,15 @@ try {
'jest/unbound-method': 'error',
},
},
{
files: 'packages/jest-types/src/Circus.ts',
rules: {
// We're faking nominal types
'@typescript-eslint/no-duplicate-type-constituents': 'off',
// this file has `Exception`, which is `unknown`
'@typescript-eslint/no-redundant-type-constituents': 'off',
},
},
],
parser: '@typescript-eslint/parser',
parserOptions: {
@ -109,6 +121,14 @@ try {
rules: {
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/dot-notation': 'error',
'@typescript-eslint/no-base-to-string': [
'error',
// https://github.com/typescript-eslint/typescript-eslint/issues/1655#issuecomment-593639305
{ignoredTypeNames: ['AssertionError', 'Error']},
],
'@typescript-eslint/no-duplicate-type-constituents': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'error',
'@typescript-eslint/no-useless-template-literals': 'error',
'@typescript-eslint/non-nullable-type-assertion-style': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-readonly': 'error',
@ -119,18 +139,17 @@ try {
'@typescript-eslint/strict-boolean-expressions': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
// TODO: enable these
// TODO: enable this
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/no-duplicate-type-constituents': 'off',
'@typescript-eslint/no-base-to-string': 'off',
// disable the ones we disable in main config
'@typescript-eslint/no-invalid-void-type': 'off',
'@typescript-eslint/no-dynamic-delete': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
// nah
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/require-await': 'off',
},
},

View File

@ -3040,6 +3040,7 @@ __metadata:
eslint-plugin-local: "link:./.eslintplugin"
eslint-plugin-markdown: ^3.0.0
eslint-plugin-prettier: ^5.0.0
eslint-plugin-promise: ^6.1.1
eslint-plugin-unicorn: ^50.0.0
execa: ^5.0.0
find-process: ^1.4.1
@ -9647,6 +9648,15 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-promise@npm:^6.1.1":
version: 6.1.1
resolution: "eslint-plugin-promise@npm:6.1.1"
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
checksum: 46b9a4f79dae5539987922afc27cc17cbccdecf4f0ba19c0ccbf911b0e31853e9f39d9959eefb9637461b52772afa1a482f1f87ff16c1ba38bdb6fcf21897e9a
languageName: node
linkType: hard
"eslint-plugin-unicorn@npm:^50.0.0":
version: 50.0.1
resolution: "eslint-plugin-unicorn@npm:50.0.1"