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

login
register
mail settings
Submitter phabricator
Date March 3, 2018, 10:26 p.m.
Message ID <b563f25252dd6f4528680999e63d0ae0@localhost.localdomain>
Download mbox | patch
Permalink /patch/28852/
State Not Applicable
Headers show

Comments

phabricator - March 3, 2018, 10:26 p.m.
ryanmce updated this revision to Diff 6485.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2608?vs=6484&id=6485

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

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

CHANGE DETAILS




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

Patch

diff --git a/tests/test-parse-errors.t b/tests/test-parse-errors.t
new file mode 100644
--- /dev/null
+++ b/tests/test-parse-errors.t
@@ -0,0 +1,19 @@ 
+  $ hg init repo
+  $ cd repo
+  $ echo a > a
+  $ hg ci -qAm aa
+
+simple error
+  $ hg log -r . -T '{shortest(node}'
+  hg: parse error at 15: unexpected token: end
+  ({shortest(node}
+                 ^ here)
+  [255]
+
+multi-line
+  $ hg log -r . -T 'line 1
+  > line2
+  > {shortest(node}
+  > line4\nline5'
+  hg: parse error at 28: unexpected token: end
+  [255]
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{
+     ^ here)
   [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
+   ^ 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
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -212,35 +212,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):