Skip to content

Commit

Permalink
Grammar: only un-escape \\ in situations where escaping is required
Browse files Browse the repository at this point in the history
This simplifies situations where one wants to obtain a string containing
multiple backslashes.
  • Loading branch information
odelalleau committed May 6, 2021
1 parent e3d3bcf commit 58821c7
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 22 deletions.
59 changes: 47 additions & 12 deletions omegaconf/grammar/OmegaConfGrammarLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@ fragment ESC_BACKSLASH: '\\\\'; // escaped backslash

TOP_INTER_OPEN: INTER_OPEN -> type(INTER_OPEN), pushMode(INTERPOLATION_MODE);

ESC_INTER: '\\${';
TOP_ESC: ESC_BACKSLASH+ -> type(ESC);
// Regular string: anything that does not contain any $ and does not end with \
// (this ensures this rule will not consume characters required to recognize other tokens).
ANY_STR: ~[$]* ~[\\$];

// Escaped interpolation: '\${', optionally preceded by an even number of \
ESC_INTER: ESC_BACKSLASH* '\\${';

// Escaped backslashes: even number of \ followed by an interpolation.
// The interpolation must *not* be matched by this rule (this is why we use a predicate lookahead).
TOP_ESC: ESC_BACKSLASH+ { self._input.LA(1) == ord("$") and self._input.LA(2) == ord("{") }? -> type(ESC);

// Other backslashes that will not need escaping.
BACKSLASHES: '\\'+ -> type(ANY_STR);

// The dollar sign must be singled out so that we can recognize interpolations.
DOLLAR: '$' -> type(ANY_STR);

// The backslash and dollar characters must not be grouped with others, so that
// we can properly detect the tokens above.
SPECIAL_CHAR: [\\$];
ANY_STR: ~[\\$]+; // anything else

////////////////
// VALUE_MODE //
Expand Down Expand Up @@ -96,11 +106,26 @@ mode QUOTED_SINGLE_MODE;
QSINGLE_INTER_OPEN: INTER_OPEN -> type(INTER_OPEN), pushMode(INTERPOLATION_MODE);
MATCHING_QUOTE_CLOSE: '\'' -> popMode;

QSINGLE_ESC: ('\\\'' | ESC_BACKSLASH)+ -> type(ESC);
// Regular string: anything that does not contain any $ *or quote* and does not end with \
QSINGLE_STR: ~['$]* ~['\\$] -> type(ANY_STR);

QSINGLE_ESC_INTER: ESC_INTER -> type(ESC_INTER);

QSINGLE_SPECIAL_CHAR: SPECIAL_CHAR -> type(SPECIAL_CHAR);
QSINGLE_STR: ~['\\$]+ -> type(ANY_STR);
// In a quoted string we also have the following escape sequences:
// - \', optionally preceded by an even number of \ (escaped quote)
// - an even number of \ followed by either the closing quote (trailing backslashes) or by
// an interpolation (as in `DEFAULT_MODE`) -- which must not be matched by this rule
QSINGLE_ESC:
(
ESC_BACKSLASH* '\\\'' |
ESC_BACKSLASH+ {(
self._input.LA(1) == ord("'") or
(self._input.LA(1) == ord("$") and self._input.LA(2) == ord("{"))
)}?
) -> type(ESC);

QSINGLE_BACKSLASHES: '\\'+ -> type(ANY_STR);
QSINGLE_DOLLAR: '$' -> type(ANY_STR);


////////////////////////
Expand All @@ -114,8 +139,18 @@ mode QUOTED_DOUBLE_MODE;
QDOUBLE_INTER_OPEN: INTER_OPEN -> type(INTER_OPEN), pushMode(INTERPOLATION_MODE);
QDOUBLE_CLOSE: '"' -> type(MATCHING_QUOTE_CLOSE), popMode;

QDOUBLE_ESC: ('\\"' | ESC_BACKSLASH)+ -> type(ESC);
QDOUBLE_STR: ~["$]* ~["\\$] -> type(ANY_STR);

QDOUBLE_ESC_INTER: ESC_INTER -> type(ESC_INTER);

QDOUBLE_SPECIAL_CHAR: SPECIAL_CHAR -> type(SPECIAL_CHAR);
QDOUBLE_STR: ~["\\$]+ -> type(ANY_STR);
QDOUBLE_ESC:
(
ESC_BACKSLASH* '\\"' |
ESC_BACKSLASH+ {(
self._input.LA(1) == ord('"') or
(self._input.LA(1) == ord("$") and self._input.LA(2) == ord("{"))
)}?
) -> type(ESC);

QDOUBLE_BACKSLASHES: '\\'+ -> type(ANY_STR);
QDOUBLE_DOLLAR: '$' -> type(ANY_STR);
2 changes: 1 addition & 1 deletion omegaconf/grammar/OmegaConfGrammarParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ singleElement: element EOF;

// Composite text expression (may contain interpolations).

text: (interpolation | ESC | ESC_INTER | SPECIAL_CHAR | ANY_STR)+;
text: (interpolation | ESC | ESC_INTER | ANY_STR)+;


// Elements.
Expand Down
7 changes: 5 additions & 2 deletions omegaconf/grammar_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def visitSingleElement(
return self.visit(ctx.getChild(0))

def visitText(self, ctx: OmegaConfGrammarParser.TextContext) -> Any:
# (interpolation | ESC | ESC_INTER | SPECIAL_CHAR | ANY_STR)+
# (interpolation | ESC | ESC_INTER | ANY_STR)+

# Single interpolation? If yes, return its resolved value "as is".
if ctx.getChildCount() == 1:
Expand Down Expand Up @@ -356,7 +356,10 @@ def _unescape(
if s.type == OmegaConfGrammarLexer.ESC:
chrs.append(s.text[1::2])
elif s.type == OmegaConfGrammarLexer.ESC_INTER:
chrs.append(s.text[1:])
# `ESC_INTER` is of the form `\\...\${`: the formula below computes
# the number of characters to keep at the end of the string to remove
# the correct number of backslashes.
chrs.append(s.text[-(len(s.text) // 2 + 1) :])
else:
chrs.append(s.text)
else:
Expand Down
24 changes: 17 additions & 7 deletions tests/test_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,14 @@
("str_quoted_too_many_3", "''a''", GrammarParseError),
("str_quoted_trailing_esc_1", r"'abc\\'", r" abc\ ".strip()),
("str_quoted_trailing_esc_2", r"'abc\\\\'", r" abc\\ ".strip()),
("str_quoted_no_esc_1", r'"abc\def"', r"abc\def"),
("str_quoted_no_esc_2", r'"abc\\def"', r"abc\def"),
("str_quoted_no_esc_3", r'"\\\abc\def"', r"\\abc\def"),
("str_quoted_no_esc_single_1", r"'abc\def'", r"abc\def"),
("str_quoted_no_esc_single_2", r"'abc\\def'", r"abc\\def"),
("str_quoted_no_esc_single_3", r"'\\\abc\def'", r"\\\abc\def"),
("str_quoted_no_esc_dollar_single", r"'abc\\$$'", r"abc\\$$"),
("str_quoted_no_esc_double_1", r'"abc\def"', r"abc\def"),
("str_quoted_no_esc_double_2", r'"abc\\def"', r"abc\\def"),
("str_quoted_no_esc_double_3", r'"\\\abc\def"', r"\\\abc\def"),
("str_quoted_no_esc_dollar_double", r'"abc\\$$"', r"abc\\$$"),
("str_quoted_bad_1", r'"abc\"', GrammarParseError),
("str_quoted_bad_2", r'"abc\\\"', GrammarParseError),
("str_quoted_esc_quote_single_1", r"'abc\'def'", "abc'def"),
Expand Down Expand Up @@ -255,8 +260,10 @@
("str_quoted_inter", "'${null}'", "None"),
("str_quoted_esc_single_1", r"'ab\'cd\'\'${str}'", "ab'cd''hi"),
("str_quoted_esc_single_2", r"""'\\\${foo}'""", r"\${foo}"),
("str_quoted_esc_single_3", r"""'\\a_${str}\\'""", r" \\a_hi\ ".strip()),
("str_quoted_esc_double_1", r'"ab\"cd\"\"${str}"', 'ab"cd""hi'),
("str_quoted_esc_double_2", r'''"\\\${foo}"''', r"\${foo}"),
("str_quoted_esc_double_3", r'''"\\a_${str}\\"''', r" \\a_hi\ ".strip()),
("str_quoted_other_quote_double", """'double"'""", 'double"'),
("str_quoted_other_quote_single", '''"single'"''', "single'"),
("str_quoted_concat_bad_1", '"Hi "${str}', GrammarParseError),
Expand Down Expand Up @@ -357,13 +364,16 @@
("str_top_esc_inter", r"Esc: \${str}", "Esc: ${str}"),
("str_top_esc_inter_wrong_1", r"Wrong: $\{str\}", r"Wrong: $\{str\}"),
("str_top_esc_inter_wrong_2", r"Wrong: \${str\}", r"Wrong: ${str\}"),
("str_top_esc_backslash", r"Esc: \\${str}", r"Esc: \hi"),
("str_top_esc_backslash_1", r"Esc: \\${str}", r"Esc: \hi"),
("str_top_esc_backslash_2", r"Esc: \\\\${str}", r"Esc: \\hi"),
("str_top_quoted_braces_wrong", r"Wrong: \{${str}\}", r"Wrong: \{hi\}"),
("str_top_leading_dollars", r"$$${str}", "$$hi"),
("str_top_trailing_dollars", r"${str}$$$$", "hi$$$$"),
("str_top_leading_escapes", r"\\\\\${str}", r"\\${str}"),
("str_top_middle_escapes", r"abc\\\\\${str}", r"abc\\${str}"),
("str_top_trailing_escapes", "${str}" + "\\" * 5, "hi" + "\\" * 3),
("str_top_leading_escapes_1", r"\\\\\${str}", r"\\${str}"),
("str_top_leading_escapes_2", r"\\\\ \${str}", r"\\\\ ${str}"),
("str_top_middle_escapes_1", r"abc\\\\\${str}", r"abc\\${str}"),
("str_top_middle_escapes_2", r"abc\\\\ \${str}", r"abc\\\\ ${str}"),
("str_top_trailing_escapes", "${str}" + "\\" * 5, "hi" + "\\" * 5),
("str_top_concat_interpolations", "${null}${float}", "None1.2"),
("str_top_issue_617", r""" ${test: "hi\\" }"} """, r" hi\"} "),
# Whitespaces.
Expand Down

0 comments on commit 58821c7

Please sign in to comment.