Skip to content

Commit

Permalink
Throw for likely option name problems (#1275)
Browse files Browse the repository at this point in the history
* Make a start on warning for option name clashes with Command properties

* Use correct property name for lookup

* Shift clash detection into routine, still WIP

* Prevent false positive clashes when negated option

* Add tests for option name  clashes

* Refine advice
  • Loading branch information
shadowspawn committed Jun 15, 2020
1 parent 0576033 commit 42f61ca
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 0 deletions.
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();
});

0 comments on commit 42f61ca

Please sign in to comment.