changeset 36669:80d7fb6c2dec

templater: add hint to template parse errors to help locate issues Previously, we would print the error name and location, but this isn't as helpful as we can be. Let's add a hint that shows the location where we encountered the parse error. Differential Revision: https://phab.mercurial-scm.org/D2608
author Ryan McElroy <rmcelroy@fb.com>
date Sat, 03 Mar 2018 14:23:40 -0800
parents e77cee5de1c7
children 44048f1bcee5
files mercurial/templater.py tests/test-command-template.t tests/test-export.t tests/test-log.t
diffstat 4 files changed, 77 insertions(+), 27 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/templater.py	Fri Mar 02 07:17:06 2018 +0530
+++ b/mercurial/templater.py	Sat Mar 03 14:23:40 2018 -0800
@@ -213,35 +213,48 @@
     unescape = [parser.unescapestr, pycompat.identity][raw]
     pos = start
     p = parser.parser(elements)
-    while pos < stop:
-        n = min((tmpl.find(c, pos, stop) for c in sepchars),
-                key=lambda n: (n < 0, n))
-        if n < 0:
-            yield ('string', unescape(tmpl[pos:stop]), pos)
-            pos = stop
-            break
-        c = tmpl[n:n + 1]
-        bs = 0  # count leading backslashes
-        if not raw:
-            bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
-        if bs % 2 == 1:
-            # escaped (e.g. '\{', '\\\{', but not '\\{')
-            yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
-            pos = n + 1
-            continue
-        if n > pos:
-            yield ('string', unescape(tmpl[pos:n]), pos)
-        if c == quote:
-            yield ('end', None, n + 1)
-            return
+    try:
+        while pos < stop:
+            n = min((tmpl.find(c, pos, stop) for c in sepchars),
+                    key=lambda n: (n < 0, n))
+            if n < 0:
+                yield ('string', unescape(tmpl[pos:stop]), pos)
+                pos = stop
+                break
+            c = tmpl[n:n + 1]
+            bs = 0  # count leading backslashes
+            if not raw:
+                bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
+            if bs % 2 == 1:
+                # escaped (e.g. '\{', '\\\{', but not '\\{')
+                yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
+                pos = n + 1
+                continue
+            if n > pos:
+                yield ('string', unescape(tmpl[pos:n]), pos)
+            if c == quote:
+                yield ('end', None, n + 1)
+                return
 
-        parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
-        if not tmpl.endswith('}', n + 1, pos):
-            raise error.ParseError(_("invalid token"), pos)
-        yield ('template', parseres, n)
+            parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
+            if not tmpl.endswith('}', n + 1, pos):
+                raise error.ParseError(_("invalid token"), pos)
+            yield ('template', parseres, n)
 
-    if quote:
-        raise error.ParseError(_("unterminated string"), start)
+        if quote:
+            raise error.ParseError(_("unterminated string"), start)
+    except error.ParseError as inst:
+        if len(inst.args) > 1:  # has location
+            loc = inst.args[1]
+            # TODO: Opportunity for improvement! If there is a newline in the
+            # template, this hint does not point to the right place, so skip.
+            if '\n' not in tmpl:
+                # We want the caret to point to the place in the template that
+                # failed to parse, but in a hint we get a open paren at the
+                # start. Therefore, we print "loc" spaces (instead of "loc - 1")
+                # to line up the caret with the location of the error.
+                inst.hint = tmpl + '\n' + ' ' * (loc) + '^ ' + _('here')
+        raise
     yield ('end', None, pos)
 
 def _unnesttemplatelist(tree):
--- a/tests/test-command-template.t	Fri Mar 02 07:17:06 2018 +0530
+++ b/tests/test-command-template.t	Sat Mar 03 14:23:40 2018 -0800
@@ -2766,19 +2766,29 @@
 
   $ hg log -T '{date'
   hg: parse error at 1: unterminated template expansion
+  ({date
+   ^ here)
   [255]
   $ hg log -T '{date(}'
   hg: parse error at 7: not a prefix: end
+  ({date(}
+         ^ here)
   [255]
   $ hg log -T '{date)}'
   hg: parse error at 5: invalid token
+  ({date)}
+       ^ here)
   [255]
   $ hg log -T '{date date}'
   hg: parse error at 6: invalid token
+  ({date date}
+        ^ here)
   [255]
 
   $ hg log -T '{}'
   hg: parse error at 2: not a prefix: end
+  ({}
+    ^ here)
   [255]
   $ hg debugtemplate -v '{()}'
   (template
@@ -2827,10 +2837,14 @@
 
   $ hg log -T '{"date'
   hg: parse error at 2: unterminated string
+  ({"date
+    ^ here)
   [255]
 
   $ hg log -T '{"foo{date|?}"}'
   hg: parse error at 11: syntax error
+  ({"foo{date|?}"}
+             ^ here)
   [255]
 
 Thrown an error if a template function doesn't exist
@@ -3362,6 +3376,8 @@
   -4
   $ hg debugtemplate '{(-)}\n'
   hg: parse error at 3: not a prefix: )
+  ({(-)}\n
+     ^ here)
   [255]
   $ hg debugtemplate '{(-a)}\n'
   hg: parse error: negation needs an integer argument
@@ -3527,6 +3543,8 @@
   foo
   $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
   hg: parse error at 21: unterminated string
+  ({if(rev, "{if(rev, \")}")}\n
+                       ^ here)
   [255]
   $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
   hg: parse error: trailing \ in string
--- a/tests/test-export.t	Fri Mar 02 07:17:06 2018 +0530
+++ b/tests/test-export.t	Sat Mar 03 14:23:40 2018 -0800
@@ -218,6 +218,8 @@
   [255]
   $ hg export -o '%m{' tip
   hg: parse error at 3: unterminated template expansion
+  (%m{
+     ^ here)
   [255]
   $ hg export -o '%\' tip
   abort: invalid format spec '%\' in output filename
--- a/tests/test-log.t	Fri Mar 02 07:17:06 2018 +0530
+++ b/tests/test-log.t	Sat Mar 03 14:23:40 2018 -0800
@@ -2289,6 +2289,23 @@
   $ hg --config extensions.names=../names.py log -r 0 --template '{bars}\n'
   foo
 
+Templater parse errors:
+
+simple error
+  $ hg log -r . -T '{shortest(node}'
+  hg: parse error at 15: unexpected token: end
+  ({shortest(node}
+                 ^ here)
+  [255]
+
+multi-line template with error
+  $ hg log -r . -T 'line 1
+  > line2
+  > {shortest(node}
+  > line4\nline5'
+  hg: parse error at 28: unexpected token: end
+  [255]
+
   $ cd ..
 
 hg log -f dir across branches