oclif / oclif/core

dependsOn / relationships type 'all' is satisfied by a flag's own default, unlike exclusive and combinable

Open
#1,639 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

BUG P3
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:

  1. Apply the same setFromDefault skip in validateDependsOn, for consistency with validateExclusive / validateCombinable.
  2. Or, if defaults are intentionally treated as provided for dependencies, document that on dependsOn / relationships at 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.