Skip to content

Commit

Permalink
fix: Rewrite the message printer as a parser to avoid problems with r…
Browse files Browse the repository at this point in the history
…egexes in old browsers (#2785)

* fix: Rewrite the message printer as a parser to avoid problems with regexes in old browsers

* Create proud-poems-try.md

---------

Co-authored-by: Luke Steyn <lukecaan@gmail.com>
  • Loading branch information
steveluscher and lukecaan committed Jun 6, 2024
1 parent 9584ba8 commit 4f19842
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-poems-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solana/errors": patch
---

The development mode error message printer no longer fatals on Safari < 16.4.
58 changes: 55 additions & 3 deletions packages/errors/src/__tests__/message-formatter-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,70 @@ describe('getErrorMessage', () => {
);
expect(message).toBe('static error message');
});
it('interpolates variables into a error message format string', () => {
it.each([
{
expected: "Something awful happened: 'bar'. How awful!",
input: "Something $severity happened: '$foo'. How $severity!",
},
// Literal backslashes, escaped dollar signs
{
expected: 'How \\awful\\ is the $severity?',
input: 'How \\\\$severity\\\\ is the \\$severity?',
},
// Variable at beginning of sequence
{ expected: 'awful times!', input: '$severity times!' },
// Variable at end of sequence
{ expected: "Isn't it awful?", input: "Isn't it $severity?" },
// Variable in middle of text sequence
{ expected: '~awful~', input: '~$severity~' },
// Variable interpolation with no value in the lookup
{ expected: 'Is $thing a sandwich?', input: 'Is $thing a sandwich?' },
// Variable that has, as a substring, some other value in the lookup
{ expected: '$fool', input: '$fool' },
// Trick for butting a variable up against regular text
{ expected: 'barl', input: '$foo\\l' },
// Escaped variable marker
{ expected: "It's the $severity, ya hear?", input: "It's the \\$severity, ya hear?" },
// Single dollar sign
{ expected: ' $ ', input: ' $ ' },
// Single dollar sign at start
{ expected: '$ ', input: '$ ' },
// Single dollar sign at end
{ expected: ' $', input: ' $' },
// Double dollar sign with legitimate variable name
{ expected: ' $bar ', input: ' $$foo ' },
// Double dollar sign with legitimate variable name at start
{ expected: '$bar ', input: '$$foo ' },
// Double dollar sign with legitimate variable name at end
{ expected: ' $bar', input: ' $$foo' },
// Single escape sequence
{ expected: ' ', input: ' \\ ' },
// Single escape sequence at start
{ expected: ' ', input: '\\ ' },
// Single escape sequence at end
{ expected: ' ', input: ' \\' },
// Double escape sequence
{ expected: ' \\ ', input: ' \\\\ ' },
// Double escape sequence at start
{ expected: '\\ ', input: '\\\\ ' },
// Double escape sequence at end
{ expected: ' \\', input: ' \\\\' },
// Just text
{ expected: 'Some unencumbered text.', input: 'Some unencumbered text.' },
// Empty string
{ expected: '', input: '' },
])('interpolates variables into the error message format string `"$input"`', ({ input, expected }) => {
const messagesSpy = jest.spyOn(MessagesModule, 'SolanaErrorMessages', 'get');
messagesSpy.mockReturnValue({
// @ts-expect-error Mock error config doesn't conform to exported config.
123: "Something $severity happened: '$foo'. How $severity!",
123: input,
});
const message = getErrorMessage(
// @ts-expect-error Mock error context doesn't conform to exported context.
123,
{ foo: 'bar', severity: 'awful' },
);
expect(message).toBe("Something awful happened: 'bar'. How awful!");
expect(message).toBe(expected);
});
it('interpolates a Uint8Array variable into a error message format string', () => {
const messagesSpy = jest.spyOn(MessagesModule, 'SolanaErrorMessages', 'get');
Expand Down
76 changes: 72 additions & 4 deletions packages/errors/src/message-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,83 @@ import { SolanaErrorCode } from './codes';
import { encodeContextObject } from './context';
import { SolanaErrorMessages } from './messages';

const enum StateType {
EscapeSequence,
Text,
Variable,
}
type State = Readonly<{
[START_INDEX]: number;
[TYPE]: StateType;
}>;
const START_INDEX = 'i';
const TYPE = 't';

export function getHumanReadableErrorMessage<TErrorCode extends SolanaErrorCode>(
code: TErrorCode,
context: object = {},
): string {
const messageFormatString = SolanaErrorMessages[code];
const message = messageFormatString.replace(/(?<!\\)\$(\w+)/g, (substring, variableName) =>
variableName in context ? `${context[variableName as keyof typeof context]}` : substring,
);
return message;
if (messageFormatString.length === 0) {
return '';
}
let state: State;
function commitStateUpTo(endIndex?: number) {
if (state[TYPE] === StateType.Variable) {
const variableName = messageFormatString.slice(state[START_INDEX] + 1, endIndex);
fragments.push(
variableName in context ? `${context[variableName as keyof typeof context]}` : `$${variableName}`,
);
} else if (state[TYPE] === StateType.Text) {
fragments.push(messageFormatString.slice(state[START_INDEX], endIndex));
}
}
const fragments: string[] = [];
messageFormatString.split('').forEach((char, ii) => {
if (ii === 0) {
state = {
[START_INDEX]: 0,
[TYPE]:
messageFormatString[0] === '\\'
? StateType.EscapeSequence
: messageFormatString[0] === '$'
? StateType.Variable
: StateType.Text,
};
return;
}
let nextState;
switch (state[TYPE]) {
case StateType.EscapeSequence:
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Text };
break;
case StateType.Text:
if (char === '\\') {
nextState = { [START_INDEX]: ii, [TYPE]: StateType.EscapeSequence };
} else if (char === '$') {
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Variable };
}
break;
case StateType.Variable:
if (char === '\\') {
nextState = { [START_INDEX]: ii, [TYPE]: StateType.EscapeSequence };
} else if (char === '$') {
commitStateUpTo(ii);
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Variable };
} else if (!char.match(/\w/)) {
nextState = { [START_INDEX]: ii, [TYPE]: StateType.Text };
}
break;
}
if (nextState) {
if (state[TYPE] !== nextState[TYPE]) {
commitStateUpTo(ii);