fix(command): don't mutate stored options when registering#10715
fix(command): don't mutate stored options when registering#10715kyungseopk1m wants to merge 1 commit into
Conversation
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
There was a problem hiding this comment.
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.
| // 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; |
There was a problem hiding this comment.
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.
| const makeClient = () => ({ cli: new Program() }) as unknown as CLIClient; | |
| const makeClient = (): CLIClient => ({ | |
| cli: new Program(), | |
| errorOut: () => {}, | |
| }); |
References
- Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| expect((command as unknown as { options: unknown[][] }).options).to.deep.equal([ | ||
| ["-x, --foobar", "description", "value"], | ||
| ["-f, --force", "automatically accept all interactive prompts"], | ||
| ]); |
There was a problem hiding this comment.
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.
| 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
- Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
register()usedargs.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 passedundefinedto commander and threwCannot 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