Skip to content

Commit

Permalink
feat: parse error messages to suggest correct flag value usage
Browse files Browse the repository at this point in the history
  • Loading branch information
WillieRuemmele committed Jul 18, 2024
1 parent b6df968 commit 05c25ba
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/sfCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,30 @@ export abstract class SfCommand<T> extends Command {
const sfCommandError = SfCommandError.from(error, this.statics.name, this.warnings);
process.exitCode = sfCommandError.exitCode;

// no var args (strict = true || undefined), and unexpected arguments when parsing
if (
this.statics.strict !== false &&
sfCommandError.exitCode === 2 &&
error.message.includes('Unexpected argument')
) {
// @ts-expect-error error's causes aren't typed, this is what's returned from flag parsing errors
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const output =
(sfCommandError.cause?.parse?.output?.raw as Array<{ flag: string; input: string; type: 'flag' | 'arg' }>) ??
[];

// find the extra arguments causing issues
const extras = output
.filter((f) => f.type === 'arg')
.flatMap((f) => f.input)
.join(' ');
// find the flag before the 'args' block that's valid, to append the args with its value as a suggestion
const target = output.find((flag, index) => flag.type === 'flag' && output[index + 1]?.type === 'arg');

sfCommandError.actions ??= [];
sfCommandError.actions.push(`--${target?.flag} "${target?.input} ${extras}"`);
}

if (this.jsonEnabled()) {
this.logJson(sfCommandError.toJson());
} else {
Expand Down
80 changes: 80 additions & 0 deletions test/unit/sfCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ class NonJsonCommand extends SfCommand<void> {
}
}

class SuggestionCommand extends SfCommand<void> {
public static enableJsonFlag = false;
public static readonly flags = {
first: Flags.string({
default: 'My first flag',
required: true,
}),
second: Flags.string({
default: 'My second',
required: true,
}),
};
public async run(): Promise<void> {
await this.parse(SuggestionCommand);
}
}

describe('jsonEnabled', () => {
afterEach(() => {
delete process.env.SF_CONTENT_TYPE;
Expand Down Expand Up @@ -375,6 +392,69 @@ describe('error standardization', () => {
}
});

it('should log correct suggestion when user doesnt wrap with quotes', async () => {
const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr');
try {
await SuggestionCommand.run(['--first', 'my', 'alias', 'with', 'spaces', '--second', 'my second value']);
expect(false, 'error should have been thrown').to.be.true;
} catch (e: unknown) {
expect(e).to.be.instanceOf(SfCommandError);
const err = e as SfCommand.Error;

// Ensure the error was logged to the console
expect(logToStderrStub.callCount).to.equal(1);
expect(logToStderrStub.firstCall.firstArg).to.contain(err.message);

// Ensure the error has expected properties
expect(err).to.have.property('actions');
expect(err.actions).to.deep.equal(['--first "my alias with spaces"']);
expect(err).to.have.property('exitCode', 2);
expect(err).to.have.property('context', 'SuggestionCommand');
expect(err).to.have.property('data', undefined);
expect(err).to.have.property('cause');
expect(err).to.have.property('code', '2');
expect(err).to.have.property('status', 2);
expect(err).to.have.property('stack').and.be.ok;
expect(err).to.have.property('skipOclifErrorHandling', true);
expect(err).to.have.deep.property('oclif', { exit: 2 });

// Ensure a sfCommandError event was emitted with the expected data
expect(sfCommandErrorData[0]).to.equal(err);
expect(sfCommandErrorData[1]).to.equal('suggestioncommand');
}
});
it('should log correct suggestion when user doesnt wrap with quotes without flag order', async () => {
const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr');
try {
await SuggestionCommand.run(['--second', 'my second value', '--first', 'my', 'alias', 'with', 'spaces']);
expect(false, 'error should have been thrown').to.be.true;
} catch (e: unknown) {
expect(e).to.be.instanceOf(SfCommandError);
const err = e as SfCommand.Error;

// Ensure the error was logged to the console
expect(logToStderrStub.callCount).to.equal(1);
expect(logToStderrStub.firstCall.firstArg).to.contain(err.message);

// Ensure the error has expected properties
expect(err).to.have.property('actions');
expect(err.actions).to.deep.equal(['--first "my alias with spaces"']);
expect(err).to.have.property('exitCode', 2);
expect(err).to.have.property('context', 'SuggestionCommand');
expect(err).to.have.property('data', undefined);
expect(err).to.have.property('cause');
expect(err).to.have.property('code', '2');
expect(err).to.have.property('status', 2);
expect(err).to.have.property('stack').and.be.ok;
expect(err).to.have.property('skipOclifErrorHandling', true);
expect(err).to.have.deep.property('oclif', { exit: 2 });

// Ensure a sfCommandError event was emitted with the expected data
expect(sfCommandErrorData[0]).to.equal(err);
expect(sfCommandErrorData[1]).to.equal('suggestioncommand');
}
});

it('should log correct error when command throws an SfError --json', async () => {
const logJsonStub = $$.SANDBOX.stub(SfCommand.prototype, 'logJson');
try {
Expand Down

0 comments on commit 05c25ba

Please sign in to comment.