D2608: templater: add hint to template parse errors to help locate issues

ryanmce (Ryan McElroy) phabricator at mercurial-scm.org
Sat Mar 3 21:49:45 UTC 2018


ryanmce created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  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.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D2608

AFFECTED FILES
  mercurial/templater.py
  tests/test-command-template.t
  tests/test-export.t

CHANGE DETAILS

diff --git a/tests/test-export.t b/tests/test-export.t
--- a/tests/test-export.t
+++ b/tests/test-export.t
@@ -218,6 +218,8 @@
   [255]
   $ hg export -o '%m{' tip
   hg: parse error at 3: unterminated template expansion
+  (%m{
+     ^)
   [255]
   $ hg export -o '%\' tip
   abort: invalid format spec '%\' in output filename
diff --git a/tests/test-command-template.t b/tests/test-command-template.t
--- a/tests/test-command-template.t
+++ b/tests/test-command-template.t
@@ -2766,19 +2766,29 @@
 
   $ hg log -T '{date'
   hg: parse error at 1: unterminated template expansion
+  ({date
+   ^)
   [255]
   $ hg log -T '{date(}'
   hg: parse error at 7: not a prefix: end
+  ({date(}
+         ^)
   [255]
   $ hg log -T '{date)}'
   hg: parse error at 5: invalid token
+  ({date)}
+       ^)
   [255]
   $ hg log -T '{date date}'
   hg: parse error at 6: invalid token
+  ({date date}
+        ^)
   [255]
 
   $ hg log -T '{}'
   hg: parse error at 2: not a prefix: end
+  ({}
+    ^)
   [255]
   $ hg debugtemplate -v '{()}'
   (template
@@ -2827,10 +2837,14 @@
 
   $ hg log -T '{"date'
   hg: parse error at 2: unterminated string
+  ({"date
+    ^)
   [255]
 
   $ hg log -T '{"foo{date|?}"}'
   hg: parse error at 11: syntax error
+  ({"foo{date|?}"}
+             ^)
   [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
+     ^)
   [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
+                       ^)
   [255]
   $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
   hg: parse error: trailing \ in string
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -212,35 +212,45 @@
     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]
+            # 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) + '^'
+        raise
     yield ('end', None, pos)
 
 def _unnesttemplatelist(tree):



To: ryanmce, #hg-reviewers
Cc: mercurial-devel


More information about the Mercurial-devel mailing list