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

Throw for likely option name problems #1275

Merged
merged 7 commits into from Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions index.js
Expand Up @@ -118,6 +118,7 @@ class Command extends EventEmitter {
this._name = name || '';
this._optionValues = {};
this._storeOptionsAsProperties = true; // backwards compatible by default
this._storeOptionsAsPropertiesCalled = false;
this._passCommandToAction = true; // backwards compatible by default
this._actionResults = [];
this._actionHandler = null;
Expand Down Expand Up @@ -430,6 +431,47 @@ class Command extends EventEmitter {
return this;
};

/**
* Internal routine to check whether there is a clash storing option value with a Command property.
*
* @param {Option} option
* @api private
*/

_checkForOptionNameClash(option) {
if (!this._storeOptionsAsProperties || this._storeOptionsAsPropertiesCalled) {
// Storing options safely, or user has been explicit and up to them.
return;
}
// User may override help, and hard to tell if worth warning.
if (option.name() === 'help') {
return;
}

const commandProperty = this._getOptionValue(option.attributeName());
if (commandProperty === undefined) {
// no clash
return;
}

let foundClash = true;
if (option.negate) {
// It is ok if define foo before --no-foo.
const positiveLongFlag = option.long.replace(/^--no-/, '--');
foundClash = !this._findOption(positiveLongFlag);
} else if (option.long) {
const negativeLongFlag = option.long.replace(/^--/, '--no-');
foundClash = !this._findOption(negativeLongFlag);
}

if (foundClash) {
throw new Error(`option '${option.name()}' clashes with existing property '${option.attributeName()}' on Command
- call storeOptionsAsProperties(false) to store option values safely,
- or call storeOptionsAsProperties(true) to suppress this check,
- or change option name`);
}
};

/**
* Internal implementation shared by .option() and .requiredOption()
*
Expand All @@ -448,6 +490,8 @@ class Command extends EventEmitter {
const name = option.attributeName();
option.mandatory = !!config.mandatory;

this._checkForOptionNameClash(option);

// default as 3rd arg
if (typeof fn !== 'function') {
if (fn instanceof RegExp) {
Expand Down Expand Up @@ -612,6 +656,7 @@ class Command extends EventEmitter {
*/

storeOptionsAsProperties(value) {
this._storeOptionsAsPropertiesCalled = true;
this._storeOptionsAsProperties = (value === undefined) || value;
if (this.options.length) {
throw new Error('call .storeOptionsAsProperties() before adding options');
Expand Down
52 changes: 52 additions & 0 deletions tests/options.detectClash.test.js
@@ -0,0 +1,52 @@
const commander = require('../');

// Check detection of likely name clashes

test('when option clashes with property then throw', () => {
const program = new commander.Command();
expect(() => {
program.option('-n, --name <name>');
}).toThrow();
});

test('when option clashes with property and storeOptionsAsProperties(true) then ok', () => {
const program = new commander.Command();
program.storeOptionsAsProperties(true);
expect(() => {
program.option('-n, --name <name>');
}).not.toThrow();
});

test('when option would clash with property but storeOptionsAsProperties(false) then ok', () => {
const program = new commander.Command();
program.storeOptionsAsProperties(false);
expect(() => {
program.option('-n, --name <name>');
}).not.toThrow();
});

test('when negated option clashes with property then throw', () => {
const program = new commander.Command();
expect(() => {
program.option('-E, --no-emit');
}).toThrow();
});

test('when positive and negative option then ok', () => {
const program = new commander.Command();
expect(() => {
program
.option('-c, --colour', 'red')
.option('-C, --no-colour');
}).not.toThrow();
});

test('when negative and positive option then ok', () => {
// Not a likely pattern, but possible and not an error.
const program = new commander.Command();
expect(() => {
program
.option('-C, --no-colour')
.option('-c, --colour');
}).not.toThrow();
});