Compiler: Incorrect parsing when string contains }} (two closing braces)
Version
3.0.5
Reproduction link
Vue 3 Template Explorer (credit: @ota-meshi)
Steps to reproduce
Use two consecutive closing braces in a string, inside a template.
What is expected?
Correct parsing.
What is actually happening?
Vue template compilation error: Error parsing JavaScript expression: Unterminated string constant (1:5)
This will happen frequently when using something like ICU message formatting, as seen in the example at the URL above.
(initially reported @ https://github.com/vuejs/vue-eslint-parser/issues/94)
The Vue compiler is not a JavaScript compiler, and the state machine does not include the JavaScript syntax environment, otherwise it will be very complicated. This still doesn't work in Vue2, and I think it can be solved by configuring delimiters
.
it will be very complicated
yes, it's very complicated to determine which is the end tag in this case.
function parseInterpolation(
context: ParserContext,
mode: TextModes
): InterpolationNode | undefined {
const [open, close] = context.options.delimiters
__TEST__ && assert(startsWith(context.source, open))
// it's very complicated to determine which is the end tag.
const closeIndex = context.source.indexOf(close, open.length)
if (closeIndex === -1) {
emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
return undefined
}
hmm that's not escaping, that's just making invalid syntax. I can do the same with } }
(space). With escaping, the AST text node is not supposed to contain the escape character.
Not the same, for example:
console.log('}\}') // '}}'
But the following is not:
console.log('} }') // '} }'
I think what @HcySunYang said makes sense and escaping them inside strings is the way to go. Especially because it's only needed when two curly braces are together
So how would u deal with this scenario?
<p>{{ someFn({ a: {b: 1}}) }}</p>
Not sure why this issue is already closed?
@posva Why are you closing this issue? This is a clear and obvious bug.
As @longlho stated, placing a backslash between the braces is not escaping anything, it's just avoiding the issue by preventing two braces from being positioned next to each other.
Please reopen.
I mean they have the same root cause :) and also IF there's a escape sequence it'd be nice to have double escaping as well.
Interpolation braces are parsed in same way as browser parses <script>
tag, basically it terminates on first occurrence of closing sequence. For including }}
in string use concatenation '}'+'}'
and use block comment for other cases }/**/}
(or consider extracting the expression as a method or computed property).
hmm I'm not sure I understand what you mean by Interpolation braces are parsed in same way as browser parses <script> tag, basically it terminates on first occurrence of closing sequence.
, any DSL has a way of escaping syntax token so reserved tokens can be used. For HTML it's the HTML entities. Escaping is fine, the point is that parser is supposed to unescape when it parses, which is not the case here. Otherwise this subjects to a whole slew of potential double escaping issues for users.
Now if the decision is to not provide escapes at all, which is fine, I think the action item is to highlight that in the doc properly, so related issues can be linked and not re-discovered.
Contents of interpolation braces is JavaScript, providing escape sequence means custom syntax which would break JS tools and IDE.
This should be documented though. cc @vuejs/docs
I think it'd be great if u guys can formalize a spec for vue template. I mean like EBNF-style spec rather than "everything inside {{ }} is a black box". I assume it can also be TypeScript/CoffeeScript in there?
Regarding breaking JS tools, as I mentioned, if u unescape before piping to JS parser (in the AST) it should work no?
I started writing spec (https://github.com/vuejs/docs-next/pull/729), it follows HTML spec like format.
Yes, we can unescape in AST but it will break eslint, typescript, babel vetur (pretty much everything). Parsing of interpolation braces is designed same as <script>
parsing of HTML (e.g. https://jsfiddle.net/znck/L375htkg/).
I assume it can also be TypeScript/CoffeeScript in there?
As far as I know (and I could be wrong), it's limited to JavaScript.
So, can we close this now as we don't see a way to effienctly detect this?
HTML likely avoids attempting to parse the contents of script elements, because it could be any language (although it's most often JavaScript), and it should be possible to parse an HTML document without having access to one of many potential script parsers.
It's somewhat different for Vue templates though, because it only supports JavaScript. So, in theory, it could ship a JS parser and parse the interpolations. Hitting an invalid token (}}
or the currently defined closing delimiter) would signal the end of the interpolation. This could also be useful for other things, like checking for valid JS syntax, etc.
The benefits would have to be weighed against:
- The dependency on a JS parser (although this could probably be an optional dependency).
- Some additional overhead when compiling templates (although this could certainly be opt-in if necessary).
I'd be interested to know what Evan thinks about this.
If there is no interest, then I'm okay with this issue being closed (although it would probably be a good idea to document this limitation).
like checking for valid JS syntax, etc.
I also thought of this and I found a library to check js syntax:https://github.com/jquery/esprima. maybe you can get some inspiration from this library.
I think we can close this once the docs has been updated since that seems to be the consensus for the action item here (so we can link from the formatjs side as well).
That's rather bad and closing doesn't feel like the right thing to do to me.
AFAIK:
- expressions in template can only be JS, no other language allowed.
compiler-sfc
already embeds a JS parser, namely Babel.
So when encountering a {{
interpolation marker, why not parse a JS expression starting on next char, then check if the expression is properly closed with }}
and throw an error if the JS is invalid or the interpolation is not properly closed.
Sure, it's more work then doing a find for the next "}}" in string, but it's the right thing to do.
}
can appear in many places inside a JS expression: inside comments /* }} */
, strings "}}"
, interpolated strings `object is {${ obj }}`
, object literals { a: { b: 4 }}
, lambda or method member bodies function () { return () => { return { g() { }}}
If you don't want to parse, another common strategy when embedding DSL like that is to provide an escaping.
Tell people that if they need to write }}
inside an interpolation they need to escape it somehow (and don't forget to provide an escape for the escape itself).
As others have mentioned in this thread, it impacts every tooling that needs to unescape the interpolated expression before processing it, so it isn't the best solution.
Just saying "find a way to write your code without }}
" is the worst solution from a user perspective.
That's essentially what I suggested above, although I wasn't aware that the compiler already included Babel, which makes this even more feasible. It's probably worth reconsidering.
expressions in template can only be JS, no other language allowed.
compiler allows additional plugins for JS expression parsing, so you can use any language supported by @babel/parser
(TypeScript and Flow are supported).
compiler-sfc already embeds a JS parser, namely Babel.
babel is only used in AOT compilation, we don't ship it in the browser version.
The parsing behaviour of interpolation is not new, it always have been like that and its modelled like <script>
tag.
Slight out of topic digression, please skip if you're here for the }}
compiler allows additional plugins for JS expression parsing, so you can use any language supported by @babel/parser (TypeScript and Flow are supported).
How does it work exactly?
- The expression itself isn't parsed, but is copied verbatim into the AOT code;
- Which is then compiled with babel + possible plugins?
So I could write TS in an expression but it wouldn't be compiled by the tsc compiler I used, types would be stripped off by babel?
Not sure why I would do this at all as there would be no type checking at all, which is the main driver of TS and Flow.
On the other hand I am now interested in the possibility of adding plugins for syntax extension such as |>
which would be nice in the context of expressions.
But does that even work with tooling? Obviously in IDE expressions wouldn't go through the same plugins as in AOT, would they?
OK, I understand why you don't want to include an extra 60K gzip of @babel/parser
just for this edge case.
You could if you wanted, still take advantage of the parser in AOT but that creates discrepancies between the two modes for things that are rare edge cases. You could make the parser opt-in in JIT, but that's also a lot of effort and complexity (options, docs, etc.) for little benefit.
If correct parsing is out of question I guess only escape and documentation remains. For reference, other frameworks have taken the escape approach. For example Aurelia doesn't fully parse its expressions but has an escape sequence.
Actually documentation might be the only thing needed as I think everything is already escape-able in pure JS (hopefully I forget nothing):
- For code like method bodies, lambda, object literals: just insert a space between
{ a: { b: 1 } }
- For strings of any kind
"
,'
or`
: you can escape it with\
. For example this:`{${ 1 }\}`
is valid and produces the desired string"{1}"
with no extra space or anything. - For comments... just deal with a space or something.
- Or put the expression into your component script, although that's not a "solution" to the issue.
Bonus points if SFC could be a little smarter and suggest the problem and its correction when it happens in a template. A heuristic might be just a matter of checking if another }}
follows without an {{
or <
in between.
its modelled like