Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(jest-validate): Allow deprecation warnings for unknown options #14499

1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@
- `[jest-resolver]` Replace unmatched capture groups in `moduleNameMapper` with empty string instead of `undefined` ([#14507](https://github.com/jestjs/jest/pull/14507))
- `[jest-snapshot]` Allow for strings as well as template literals in inline snapshots ([#14465](https://github.com/jestjs/jest/pull/14465))
- `[@jest/test-sequencer]` Calculate test runtime if `perStats.duration` is missing ([#14473](https://github.com/jestjs/jest/pull/14473))
- `[jest-validate]` Allow deprecation warnings for unknown options ([#14499](https://github.com/jestjs/jest/pull/14499))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be features I think


### Performance

Expand Down
Expand Up @@ -31,6 +31,26 @@ exports[`fails for unknown option 1`] = `
<red></color>"
`;

exports[`handles deprecated CLI options print warning for deprecated options that are listed in config 1`] = `
"<yellow><bold>foo</intensity>:</color>
<yellow></color>
<yellow>Deprecation message</color>
<yellow></color>
<yellow> <bold>CLI Options Documentation:</intensity></color>
<yellow> https://jestjs.io/docs/cli</color>
<yellow></color>"
`;

exports[`handles deprecated CLI options throw an error for deprecated options that are not listed in config 1`] = `
"<red><bold>foo</intensity>:</color>
<red></color>
<red>Deprecation message</color>
<red></color>
<red> <bold>CLI Options Documentation:</intensity></color>
<red> https://jestjs.io/docs/cli</color>
<red></color>"
`;

exports[`shows suggestion when unrecognized cli param length > 1 1`] = `
"<red><bold><bold>●</intensity><bold> Unrecognized CLI Parameter</intensity>:</color>
<red></color>
Expand Down
47 changes: 47 additions & 0 deletions packages/jest-validate/src/__tests__/validateCLIOptions.test.ts
Expand Up @@ -6,6 +6,7 @@
*
*/

import type {DeprecatedOptions} from '../types';
import validateCLIOptions from '../validateCLIOptions';

test('validates yargs special options', () => {
Expand Down Expand Up @@ -59,3 +60,49 @@ test('shows suggestion when unrecognized cli param length > 1', () => {

expect(() => validateCLIOptions(argv)).toThrowErrorMatchingSnapshot();
});

describe('handles deprecated CLI options', () => {
beforeEach(() => {
jest.spyOn(console, 'warn');
});

afterEach(() => {
jest.mocked(console.warn).mockRestore();
});

test('print warning for deprecated options that are listed in config', () => {
const optionName = 'foo';
const argv = {
$0: 'foo',
_: ['bar'],
[optionName]: true,
};

validateCLIOptions(argv, {
deprecationEntries: {
[optionName]: () => 'Deprecation message',
} as DeprecatedOptions,
[optionName]: {},
});

expect(jest.mocked(console.warn).mock.calls[0][0]).toMatchSnapshot();
});

test('throw an error for deprecated options that are not listed in config', () => {
const optionName = 'foo';

const argv = {
$0: 'foo',
_: ['bar'],
[optionName]: true,
};

expect(() =>
validateCLIOptions(argv, {
deprecationEntries: {
[optionName]: () => 'Deprecation message',
} as DeprecatedOptions,
}),
).toThrowErrorMatchingSnapshot();
});
});
2 changes: 2 additions & 0 deletions packages/jest-validate/src/types.ts
Expand Up @@ -15,6 +15,8 @@ export type DeprecatedOptionFunc = (arg: Record<string, unknown>) => string;

export type DeprecatedOptions = Record<string, DeprecatedOptionFunc>;

export type DeprecationItem = {fatal: boolean; name: string};

export type ValidationOptions = {
comment?: string;
condition?: (option: unknown, validOption: unknown) => boolean;
Expand Down
68 changes: 41 additions & 27 deletions packages/jest-validate/src/validateCLIOptions.ts
Expand Up @@ -9,10 +9,17 @@ import camelcase = require('camelcase');
import chalk = require('chalk');
import type {Options} from 'yargs';
import type {Config} from '@jest/types';
import defaultConfig from './defaultConfig';
import {deprecationWarning} from './deprecated';
import type {DeprecatedOptionFunc, DeprecatedOptions} from './types';
import {ValidationError, createDidYouMeanMessage, format} from './utils';
import type {
DeprecatedOptionFunc,
DeprecatedOptions,
DeprecationItem,
} from './types';
import {
ValidationError,
createDidYouMeanMessage,
format,
logValidationWarning,
} from './utils';

const BULLET: string = chalk.bold('\u25cf');
export const DOCUMENTATION_NOTE = ` ${chalk.bold('CLI Options Documentation:')}
Expand Down Expand Up @@ -48,16 +55,21 @@ const createCLIValidationError = (
return new ValidationError(title, message, comment);
};

const logDeprecatedOptions = (
deprecatedOptions: Array<string>,
const validateDeprecatedOptions = (
deprecatedOptions: Array<DeprecationItem>,
deprecationEntries: DeprecatedOptions,
argv: Config.Argv,
) => {
deprecatedOptions.forEach(opt => {
deprecationWarning(argv, opt, deprecationEntries, {
...defaultConfig,
comment: DOCUMENTATION_NOTE,
});
const name = opt.name;
const message = deprecationEntries[name](argv);
const comment = DOCUMENTATION_NOTE;

if (opt.fatal) {
throw new ValidationError(name, message, comment);
} else {
logValidationWarning(name, message, comment);
}
});
};

Expand All @@ -69,29 +81,19 @@ export default function validateCLIOptions(
rawArgv: Array<string> = [],
): boolean {
const yargsSpecialOptions = ['$0', '_', 'help', 'h'];
const deprecationEntries = options.deprecationEntries ?? {};

const allowedOptions = Object.keys(options).reduce(
(acc, option) =>
acc.add(option).add((options[option].alias as string) || option),
new Set(yargsSpecialOptions),
);
const unrecognizedOptions = Object.keys(argv).filter(
arg =>
!allowedOptions.has(camelcase(arg, {locale: 'en-US'})) &&
!allowedOptions.has(arg) &&
(!rawArgv.length || rawArgv.includes(arg)),
[],
);

if (unrecognizedOptions.length) {
throw createCLIValidationError(unrecognizedOptions, allowedOptions);
}

const deprecationEntries = options.deprecationEntries ?? {};
const CLIDeprecations = Object.keys(deprecationEntries).reduce<
Record<string, DeprecatedOptionFunc>
>((acc, entry) => {
acc[entry] = deprecationEntries[entry];
if (options[entry]) {
acc[entry] = deprecationEntries[entry];
const alias = options[entry].alias as string;
if (alias) {
acc[alias] = deprecationEntries[entry];
Expand All @@ -100,12 +102,24 @@ export default function validateCLIOptions(
return acc;
}, {});
const deprecations = new Set(Object.keys(CLIDeprecations));
const deprecatedOptions = Object.keys(argv).filter(
arg => deprecations.has(arg) && argv[arg] != null,
);
const deprecatedOptions = Object.keys(argv)
.filter(arg => deprecations.has(arg) && argv[arg] != null)
.map(arg => ({fatal: !allowedOptions.has(arg), name: arg}));

if (deprecatedOptions.length) {
logDeprecatedOptions(deprecatedOptions, CLIDeprecations, argv);
validateDeprecatedOptions(deprecatedOptions, CLIDeprecations, argv);
}

const unrecognizedOptions = Object.keys(argv).filter(
arg =>
!allowedOptions.has(camelcase(arg, {locale: 'en-US'})) &&
!allowedOptions.has(arg) &&
(!rawArgv.length || rawArgv.includes(arg)),
[],
);

if (unrecognizedOptions.length) {
throw createCLIValidationError(unrecognizedOptions, allowedOptions);
}

return true;
Expand Down