dependsOn / relationships type 'all' is satisfied by a flag's own default, unlike exclusive and combinable
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 316
- Forks
- 98
- Avg merge
- 12h 32m
- Merged PRs (30d)
- 2
Description
Version: @oclif/core 4.8.0
Summary
dependsOn (and relationships: [{type: 'all', ...}], which routes to the same validator) treats a flag as "provided" whenever it has a defined value after parsing. That includes values that came from the flag's own default. As a result, a dependency on any flag that declares a default can never fail — the check silently always passes.
This is inconsistent with the exclusivity validators in the same file, which explicitly skip defaulted flags.
The code
From lib/parser/validate.js in 4.8.0, validateExclusive (and identically validateCombinable) skips flags that were set from a default:
async function validateExclusive(name, flags) {
const base = { name, validationFn: 'validateExclusive' };
const resolved = await resolveFlags(flags);
const keys = getPresentFlags(resolved);
for (const flag of keys) {
// do not enforce exclusivity for flags that were defaulted
if (parse.output.metadata.flags && parse.output.metadata.flags[flag]?.setFromDefault)
continue;
if (parse.output.metadata.flags && parse.output.metadata.flags[name]?.setFromDefault)
continue;
...
validateDependsOn has no such check — it only tests for !== undefined:
async function validateDependsOn(name, flags) {
const base = { name, validationFn: 'validateDependsOn' };
const resolved = await resolveFlags(flags);
const foundAll = Object.values(resolved).every((val) => val !== undefined);
if (!foundAll) {
...
resolveFlags reads values straight out of parse.output.flags, which already has defaults applied, so a defaulted flag is always !== undefined.
And relationships of type 'all' dispatches to that same function:
function validateRelationships(name, flag) {
return (flag.relationships ?? []).map((relationship) => {
switch (relationship.type) {
case 'all': {
return validateDependsOn(name, relationship.flags);
}
case 'none': {
return validateExclusive(name, relationship.flags);
}
case 'only': {
return validateCombinable(name, relationship.flags);
}
...
So the setFromDefault distinction — "set from a default" is not the same as "the user provided this" — is applied to three of the four relationship types, but not to all/dependsOn.
Reproduction
static flags = {
mode: Flags.string({ options: ['a', 'b'], default: 'a' }),
extra: Flags.string({ dependsOn: ['mode'] }),
}
Running:
mycmd --extra=x
Expected: validation fails — --mode was never supplied, and the author's intent was "if you pass --extra you must also pass --mode".
Actual: validation passes, because mode defaulted to 'a' and is therefore !== undefined.
The same happens with the relationships spelling:
extra: Flags.string({ relationships: [{type: 'all', flags: ['mode']}] }),
Impact
The failure is silent. There is no warning at definition time and no error at runtime — the constraint simply never fires, so a CLI author believes they have validation they do not have. Any dependsOn target that carries a default is unenforceable.
We hit this in a production CLI and only discovered it by reading oclif's source after the constraint failed to reject a combination we expected it to reject.
Possible resolutions
Either would work for us:
- Apply the same
setFromDefaultskip invalidateDependsOn, for consistency withvalidateExclusive/validateCombinable. - Or, if defaults are intentionally treated as provided for dependencies, document that on
dependsOn/relationshipsat https://oclif.io/docs/flags. The current docs say only "Make this flag dependent on all of these flags", which reads as a statement about what the user passed.
Happy to send a PR for whichever direction you prefer.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/parser/validate.js and trace validateDependsOn through resolveFlags and the relationships type 'all' dispatch. Run the provided reproduction with a defaulted mode and --extra=x, then compare its behavior with the existing exclusivity checks. Done means the intended treatment of defaulted flags is decided and the reproduction plus the relationships spelling behave consistently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100