Skip to content

Commit 81cac38

Browse files
authored
Merge pull request #593 from salesforcecli/wr/smarterSuggestion
Wr/smarter suggestion
2 parents b6df968 + 12db42d commit 81cac38

File tree

3 files changed

+120
-0
lines changed

3 files changed

+120
-0
lines changed

src/SfCommandError.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,35 @@ export class SfCommandError extends SfError {
9595
result: this.result,
9696
};
9797
}
98+
99+
public appendErrorSuggestions(): void {
100+
const output =
101+
// @ts-expect-error error's causes aren't typed, this is what's returned from flag parsing errors
102+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
103+
(this.cause?.parse?.output?.raw as Array<{ flag: string; input: string; type: 'flag' | 'arg' }>) ?? [];
104+
105+
/*
106+
if there's a group of args, and additional args separated, we could have multiple suggestions
107+
--first my first --second my second =>
108+
try this:
109+
--first "my first"
110+
--second "my second"
111+
*/
112+
113+
const aggregator: Array<{ flag: string; args: string[] }> = [];
114+
output.forEach((k, i) => {
115+
let argCounter = i + 1;
116+
if (k.type === 'flag' && output[argCounter]?.type === 'arg') {
117+
const args: string[] = [];
118+
while (output[argCounter]?.type === 'arg') {
119+
args.push(output[argCounter].input);
120+
argCounter++;
121+
}
122+
aggregator.push({ flag: k.flag, args: [k.input, ...args] });
123+
}
124+
});
125+
126+
this.actions ??= [];
127+
this.actions.push(...aggregator.map((cause) => `--${cause.flag} "${cause.args.join(' ')}"`));
128+
}
98129
}

src/sfCommand.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,15 @@ export abstract class SfCommand<T> extends Command {
376376
const sfCommandError = SfCommandError.from(error, this.statics.name, this.warnings);
377377
process.exitCode = sfCommandError.exitCode;
378378

379+
// no var args (strict = true || undefined), and unexpected arguments when parsing
380+
if (
381+
this.statics.strict !== false &&
382+
sfCommandError.exitCode === 2 &&
383+
error.message.includes('Unexpected argument')
384+
) {
385+
sfCommandError.appendErrorSuggestions();
386+
}
387+
379388
if (this.jsonEnabled()) {
380389
this.logJson(sfCommandError.toJson());
381390
} else {

test/unit/sfCommand.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ class NonJsonCommand extends SfCommand<void> {
110110
}
111111
}
112112

113+
class SuggestionCommand extends SfCommand<void> {
114+
public static enableJsonFlag = false;
115+
public static readonly flags = {
116+
first: Flags.string({
117+
default: 'My first flag',
118+
required: true,
119+
}),
120+
second: Flags.string({
121+
default: 'My second',
122+
required: true,
123+
}),
124+
};
125+
public async run(): Promise<void> {
126+
await this.parse(SuggestionCommand);
127+
}
128+
}
129+
113130
describe('jsonEnabled', () => {
114131
afterEach(() => {
115132
delete process.env.SF_CONTENT_TYPE;
@@ -375,6 +392,69 @@ describe('error standardization', () => {
375392
}
376393
});
377394

395+
it('should log correct suggestion when user doesnt wrap with quotes', async () => {
396+
const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr');
397+
try {
398+
await SuggestionCommand.run(['--first', 'my', 'alias', 'with', 'spaces', '--second', 'my second', 'value']);
399+
expect(false, 'error should have been thrown').to.be.true;
400+
} catch (e: unknown) {
401+
expect(e).to.be.instanceOf(SfCommandError);
402+
const err = e as SfCommand.Error;
403+
404+
// Ensure the error was logged to the console
405+
expect(logToStderrStub.callCount).to.equal(1);
406+
expect(logToStderrStub.firstCall.firstArg).to.contain(err.message);
407+
408+
// Ensure the error has expected properties
409+
expect(err).to.have.property('actions');
410+
expect(err.actions).to.deep.equal(['--first "my alias with spaces"', '--second "my second value"']);
411+
expect(err).to.have.property('exitCode', 2);
412+
expect(err).to.have.property('context', 'SuggestionCommand');
413+
expect(err).to.have.property('data', undefined);
414+
expect(err).to.have.property('cause');
415+
expect(err).to.have.property('code', '2');
416+
expect(err).to.have.property('status', 2);
417+
expect(err).to.have.property('stack').and.be.ok;
418+
expect(err).to.have.property('skipOclifErrorHandling', true);
419+
expect(err).to.have.deep.property('oclif', { exit: 2 });
420+
421+
// Ensure a sfCommandError event was emitted with the expected data
422+
expect(sfCommandErrorData[0]).to.equal(err);
423+
expect(sfCommandErrorData[1]).to.equal('suggestioncommand');
424+
}
425+
});
426+
it('should log correct suggestion when user doesnt wrap with quotes without flag order', async () => {
427+
const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr');
428+
try {
429+
await SuggestionCommand.run(['--second', 'my second value', '--first', 'my', 'alias', 'with', 'spaces']);
430+
expect(false, 'error should have been thrown').to.be.true;
431+
} catch (e: unknown) {
432+
expect(e).to.be.instanceOf(SfCommandError);
433+
const err = e as SfCommand.Error;
434+
435+
// Ensure the error was logged to the console
436+
expect(logToStderrStub.callCount).to.equal(1);
437+
expect(logToStderrStub.firstCall.firstArg).to.contain(err.message);
438+
439+
// Ensure the error has expected properties
440+
expect(err).to.have.property('actions');
441+
expect(err.actions).to.deep.equal(['--first "my alias with spaces"']);
442+
expect(err).to.have.property('exitCode', 2);
443+
expect(err).to.have.property('context', 'SuggestionCommand');
444+
expect(err).to.have.property('data', undefined);
445+
expect(err).to.have.property('cause');
446+
expect(err).to.have.property('code', '2');
447+
expect(err).to.have.property('status', 2);
448+
expect(err).to.have.property('stack').and.be.ok;
449+
expect(err).to.have.property('skipOclifErrorHandling', true);
450+
expect(err).to.have.deep.property('oclif', { exit: 2 });
451+
452+
// Ensure a sfCommandError event was emitted with the expected data
453+
expect(sfCommandErrorData[0]).to.equal(err);
454+
expect(sfCommandErrorData[1]).to.equal('suggestioncommand');
455+
}
456+
});
457+
378458
it('should log correct error when command throws an SfError --json', async () => {
379459
const logJsonStub = $$.SANDBOX.stub(SfCommand.prototype, 'logJson');
380460
try {

0 commit comments

Comments
 (0)