From 84ef6d82546b044990f4efd57e51e29c6c6565c8 Mon Sep 17 00:00:00 2001
From: Samuel Thibault <sthibault@hypra.fr>
Date: Wed, 21 Feb 2018 15:51:11 +0100
Subject: Build external lxml if not provided by system

except on windows, where gla11y will resort to python's internal xml parser,
which does not provide line numbers.

This allows gla11y to be runnable on all systems.

Change-Id: Ica4eb90f59bddfcefd783fc2ed9c8c27357e7572
Reviewed-on: https://gerrit.libreoffice.org/50115
Tested-by: Jenkins <ci@libreoffice.org>
Reviewed-by: Stephan Bergmann <sbergman@redhat.com>
---
 bin/gla11y | 167 ++++++++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 126 insertions(+), 41 deletions(-)

(limited to 'bin')

diff --git a/bin/gla11y b/bin/gla11y
index 04d5d83ccd1a..77a84840087a 100755
--- a/bin/gla11y
+++ b/bin/gla11y
@@ -31,33 +31,84 @@ from __future__ import print_function
 import os
 import sys
 import getopt
-import lxml.etree as ET
+try:
+    import lxml.etree as ET
+    lxml = True
+except ImportError:
+    import xml.etree.ElementTree as ET
+    lxml = False
 
 progname = os.path.basename(sys.argv[0])
+outfile = None
 Werror = False
 Wnone = False
 errors = 0
 warnings = 0
 
-
-def errstr(elm):
+def step_elm(elm):
+    """
+    Return the XML class path step corresponding to elm.
+    This can be empty if the elm does not have any class or id.
+    """
+    step = elm.attrib.get('class')
+    if step is None:
+        step = ""
+    oid = elm.attrib.get('id')
+    if oid is not None:
+        oid = oid.encode('ascii','ignore').decode('ascii')
+        step += "[@id='%s']" % oid
+    if len(step) > 0:
+        step += '/'
+    return step
+
+def find_elm(root, elm):
     """
-    Print the line number of the element
+    Return the XML class path of the element from the given root.
+    This is the slow version used when getparent is not available.
     """
+    if root == elm:
+        return ""
+    for o in root:
+        path = find_elm(o, elm)
+        if path is not None:
+            step = step_elm(o)
+            return step + path
+    return None
 
-    return str(elm.sourceline)
 
-def err(filename, elm, msg):
+def elm_prefix(filename, elm):
+    """
+    Return the display prefix of the element
+    """
+    if elm == None or not lxml:
+        return "%s:" % filename
+    else:
+        return "%s:%u" % (filename, elm.sourceline)
+
+def elm_name(elm):
+    """
+    Return a display name of the element
+    """
+    if elm is not None:
+        name = ""
+        if 'class' in elm.attrib:
+            name = "'%s' " % elm.attrib['class']
+        if 'id' in elm.attrib:
+            id = elm.attrib['id'].encode('ascii','ignore').decode('ascii')
+            name += "'%s' " % id
+        return name
+    return ""
+
+def err(filename, tree, elm, msg):
     global errors
 
-    if elm == None:
-        prefix = "%s:" % filename
-    else:
-        prefix = "%s:%s" % (filename, errstr(elm))
+    prefix = elm_prefix(filename, elm)
 
     errors += 1
-    msg = "%s ERROR: %s" % (prefix, msg)
-    print(msg.encode('ascii', 'ignore'))
+    msg = "%s ERROR: %s%s" % (prefix, elm_name(elm), msg)
+    print(msg)
+    if outfile is not None:
+        print(msg, file=outfile)
 
 
 def warn(filename, elm, msg):
@@ -66,61 +117,73 @@ def warn(filename, elm, msg):
     if Wnone:
         return
 
-    prefix = "%s:%s" % (filename, errstr(elm))
+    prefix = elm_prefix(filename, elm)
 
     if Werror:
         errors += 1
     else:
         warnings += 1
 
-    msg = "%s WARNING: %s" % (prefix,  msg)
-    print(msg.encode('ascii', 'ignore'))
+    msg = "%s WARNING: %s%s" % (prefix, elm_name(elm), msg)
+    print(msg)
+    if outfile is not None:
+        print(msg, file=outfile)
 
 
-def check_objects(filename, elm, objects, target):
+def check_objects(filename, tree, elm, objects, target):
     """
     Check that objects contains exactly one object
     """
     length = len(list(objects))
     if length == 0:
-        err(filename, elm, "use of undeclared target '%s'" % target)
+        err(filename, tree, elm, "uses undeclared target '%s'" % target)
     elif length > 1:
-        err(filename, elm, "several targets are named '%s'" % target)
+        err(filename, tree, elm, "several targets are named '%s'" % target)
 
-def check_props(filename, root, props):
+def check_props(filename, tree, root, elm, props):
     """
     Check the given list of relation properties
     """
     for prop in props:
         objects = root.iterfind(".//object[@id='%s']" % prop.text)
-        check_objects(filename, prop, objects, prop.text)
+        check_objects(filename, tree, elm, objects, prop.text)
 
-def check_rels(filename, root, rels):
+def check_rels(filename, tree, root, elm, rels):
     """
     Check the given list of relations
     """
     for rel in rels:
         target = rel.attrib['target']
         targets = root.iterfind(".//object[@id='%s']" % target)
-        check_objects(filename, rel, targets, target)
+        check_objects(filename, tree, elm, targets, target)
 
-def check_a11y_relation(filename, root):
+def elms_lines(elms):
+    """
+    Return the list of lines for the given elements.
+    """
+    if lxml:
+        return ": lines " + ', '.join([str(l.sourceline) for l in elms])
+    else:
+        return ""
+
+def check_a11y_relation(filename, tree):
     """
     Emit an error message if any of the 'object' elements of the XML
     document represented by `root' doesn't comply with Accessibility
     rules.
     """
+    root = tree.getroot()
 
     for obj in root.iter('object'):
 
         label_for = obj.findall("accessibility/relation[@type='label-for']")
-        check_rels(filename, root, label_for)
+        check_rels(filename, tree, root, obj, label_for)
 
         labelled_by = obj.findall("accessibility/relation[@type='labelled-by']")
-        check_rels(filename, root, labelled_by)
+        check_rels(filename, tree, root, obj, labelled_by)
 
         member_of = obj.findall("accessibility/relation[@type='member-of']")
-        check_rels(filename, root, member_of)
+        check_rels(filename, tree, root, obj, member_of)
 
         if obj.attrib['class'] == 'GtkLabel':
             # Case 0: A 'GtkLabel' must contain one or more "label-for"
@@ -130,14 +193,13 @@ def check_a11y_relation(filename, root):
 
             # ...a single "mnemonic_widget"
             properties = obj.findall("property[@name='mnemonic_widget']")
-            check_props(filename, root, properties)
+            check_props(filename, tree, root, obj, properties)
             if len(properties) > 1:
                 # It does not make sense for a label to be a mnemonic for
                 # several actions.
-                lines = ', '.join([str(p.sourceline) for p in properties])
-                err(filename, obj, "too many sub-elements"
+                err(filename, tree, obj, "multiple-mnemonic", "has too many sub-elements"
                     ", expected single <property name='mnemonic_widgets'>"
-                    ": lines %s" % lines)
+                    "%s" % elms_lines(properties))
                 continue
             if len(properties) == 1:
                 continue
@@ -148,10 +210,9 @@ def check_a11y_relation(filename, root):
         children = obj.findall("child[@internal-child='accessible']")
         if children:
             if len(children) > 1:
-                lines = ', '.join([str(c.sourceline) for c in children])
-                err(filename, obj, "too many sub-elements"
+                err(filename, tree, obj, "multiple-accessible", "has too many sub-elements"
                     ", expected single <child internal-child='accessible'>"
-                    ": lines %s" % lines)
+                    "%s" % elms_lines(children))
             continue
 
         # Case 2: has an <accessibility> sub-element with a "labelled-by"
@@ -164,42 +225,66 @@ def check_a11y_relation(filename, root):
 
 
 def usage():
-    print("%s [-W error|none] [file ...]" % progname,
+    print("%s [-W error|none] [-o LOG_FILE] [file ... | -L filelist]" % progname,
           file=sys.stderr)
+    print("  -o Also prints errors and warnings to given file");
     sys.exit(2)
 
 
 def main():
-    global Werror, Wnone, errors
+    global Werror, Wnone, errors, outfile
 
     try:
-        opts, args = getopt.getopt(sys.argv[1:], "pW:")
+        opts, args = getopt.getopt(sys.argv[1:], "W:o:L:")
     except getopt.GetoptError:
         usage()
 
+    out = None
+    filelist = None
     for o, a in opts:
         if o == "-W":
             if a == "error":
                 Werror = True
             elif a == "none":
                 Wnone = True
+        elif o == "-o":
+            out = a
+        elif o == "-L":
+            filelist = a
+
+    if out is not None:
+        outfile = open(out, 'w')
+
+    if filelist is not None:
+        try:
+            filelistfile = open(filelist, 'r')
+            for line in filelistfile.readlines():
+                line = line.strip()
+                if line:
+                    args += line.split(' ')
+            filelistfile.close()
+        except IOError:
+            err(filelist, None, None, "unable to read file list file")
 
     for filename in args:
         try:
             tree = ET.parse(filename)
         except ET.ParseError:
-            err(filename, None, "malformatted xml file")
+            err(filename, None, None, "malformatted xml file")
+            continue
         except IOError:
-            err(filename, None, "unable to read file")
+            err(filename, None, None, "unable to read file")
+            continue
 
         try:
-            check_a11y_relation(filename, tree.getroot())
+            check_a11y_relation(filename, tree)
         except Exception as error:
             import traceback
             traceback.print_exc()
-            err(filename, None, "error parsing file")
-
+            err(filename, None, None, "error parsing file")
 
+    if outfile is not None:
+        outfile.close()
     if errors > 0:
         sys.exit(1)
 
-- 
cgit