Skip to content

Commit

Permalink
Merge pull request #593 from salesforcecli/wr/smarterSuggestion
Browse files Browse the repository at this point in the history
Wr/smarter suggestion
  • Loading branch information
shetzel authored Jul 19, 2024
2 parents b6df968 + 12db42d commit 81cac38
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/SfCommandError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,35 @@ export class SfCommandError extends SfError {
result: this.result,
};
}

public appendErrorSuggestions(): void {
const output =
// @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
(this.cause?.parse?.output?.raw as Array<{ flag: string; input: string; type: 'flag' | 'arg' }>) ?? [];

/*
if there's a group of args, and additional args separated, we could have multiple suggestions
--first my first --second my second =>
try this:
--first "my first"
--second "my second"
*/

const aggregator: Array<{ flag: string; args: string[] }> = [];
output.forEach((k, i) => {
let argCounter = i + 1;
if (k.type === 'flag' && output[argCounter]?.type === 'arg') {
const args: string[] = [];
while (output[argCounter]?.type === 'arg') {
args.push(output[argCounter].input);
argCounter++;
}
aggregator.push({ flag: k.flag, args: [k.input, ...args] });
}
});

this.actions ??= [];
this.actions.push(...aggregator.map((cause) => `--${cause.flag} "${cause.args.join(' ')}"`));
}
}
9 changes: 9 additions & 0 deletions src/sfCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,15 @@ 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')
) {
sfCommandError.appendErrorSuggestions();
}

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"', '--second "my second value"']);
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 81cac38

Please sign in to comment.