Skip to content

fix(command): don't mutate stored options when registering#10715

Open
kyungseopk1m wants to merge 1 commit into
firebase:mainfrom
kyungseopk1m:fix/command-options-no-mutate
Open

fix(command): don't mutate stored options when registering#10715
kyungseopk1m wants to merge 1 commit into
firebase:mainfrom
kyungseopk1m:fix/command-options-no-mutate

Conversation

@kyungseopk1m

Copy link
Copy Markdown

register() used args.shift(), which mutated the stored option arrays in place. Registering the same command instance a second time (e.g. when firebase-tools is imported as a module across multiple invocations) then passed undefined to commander and threw Cannot read properties of undefined (reading 'indexOf').

Switched to destructuring so the stored options aren't modified, and added a regression test that registers a command multiple times.

This is the small fix that was split out of #9935.

Fixes #9929

register() used args.shift() on the stored option arrays, so registering
the same command instance more than once (e.g. when firebase-tools is
imported as a module across multiple invocations) lost the flags and threw
"Cannot read properties of undefined (reading 'indexOf')". Use destructuring
so the stored options aren't modified.

Fixes firebase#9929

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request fixes an issue where registering a command multiple times mutated its stored options array by shifting the flags out of it. This was resolved by destructuring the options array in src/command.ts instead of using shift(). A corresponding unit test was added in src/command.spec.ts. The review feedback recommends avoiding the use of as unknown type assertions in the new test file to comply with the repository style guide, suggesting proper mock definitions and using Reflect.get to access private properties instead.

Comment thread src/command.spec.ts
// being imported as a module and used across multiple CLI invocations, where
// each runner re-registers the same cached command instance (see
// src/commands/index.ts).
const makeClient = () => ({ cli: new Program() }) as unknown as CLIClient;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The helper makeClient uses as unknown as CLIClient to cast an incomplete object to CLIClient. This violates the repository style guide rule against using any or unknown as an escape hatch. Additionally, the cast is unsafe because CLIClient requires the errorOut method, which is missing from the returned object and could cause runtime errors if the command action is ever triggered in tests.

We can define makeClient with the correct return type and provide a mock errorOut function to avoid the cast entirely.

Suggested change
const makeClient = () => ({ cli: new Program() }) as unknown as CLIClient;
const makeClient = (): CLIClient => ({
cli: new Program(),
errorOut: () => {},
});
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment thread src/command.spec.ts
Comment on lines +57 to +60
expect((command as unknown as { options: unknown[][] }).options).to.deep.equal([
["-x, --foobar", "description", "value"],
["-f, --force", "automatically accept all interactive prompts"],
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using as unknown as { options: unknown[][] } to access the private options property violates the repository style guide rule against using any or unknown as an escape hatch.

We can use Reflect.get(command, "options") to access the private property cleanly without any type assertions or escape hatches.

Suggested change
expect((command as unknown as { options: unknown[][] }).options).to.deep.equal([
["-x, --foobar", "description", "value"],
["-f, --force", "automatically accept all interactive prompts"],
]);
expect(Reflect.get(command, "options")).to.deep.equal([
["-x, --foobar", "description", "value"],
["-f, --force", "automatically accept all interactive prompts"],
]);
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Command.js mutates the options array

2 participants