[Libreoffice-commits] core.git: bin/gla11y solenv/gbuild solenv/sanitizers

Samuel Thibault sthibault at hypra.fr
Wed Feb 28 21:57:42 UTC 2018


 bin/gla11y                              |  165 ++++++++++++++++++++++++++++----
 solenv/gbuild/UIConfig.mk               |    6 +
 solenv/gbuild/platform/com_GCC_class.mk |    2 
 solenv/gbuild/platform/com_MSC_class.mk |    2 
 solenv/sanitizers/ui/cui.suppr          |    3 
 solenv/sanitizers/ui/svt.suppr          |    2 
 solenv/sanitizers/ui/svx.suppr          |    6 +
 7 files changed, 168 insertions(+), 18 deletions(-)

New commits:
commit d09cc5fe73fc1de27e92dae38bc58ea0aadb4f27
Author: Samuel Thibault <sthibault at hypra.fr>
Date:   Fri Feb 23 14:49:01 2018 +0100

    gla11y: add warning/error suppression machinery
    
    Also add an accessibility test which does have a few existing issues, and
    the corresponding suppression lines.
    
    Change-Id: I7095cdc13e40501bbdf6e635c1e4f93f70bc1316
    Reviewed-on: https://gerrit.libreoffice.org/50251
    Tested-by: Jenkins <ci at libreoffice.org>
    Reviewed-by: Stephan Bergmann <sbergman at redhat.com>

diff --git a/bin/gla11y b/bin/gla11y
index 77a84840087a..9d550a6ea001 100755
--- a/bin/gla11y
+++ b/bin/gla11y
@@ -39,11 +39,17 @@ except ImportError:
     lxml = False
 
 progname = os.path.basename(sys.argv[0])
+suppressions = {}
+gen_suppr = None
+gen_supprfile = None
 outfile = None
+pflag = False
 Werror = False
 Wnone = False
 errors = 0
+errexists = 0
 warnings = 0
+warnexists = 0
 
 def step_elm(elm):
     """
@@ -75,6 +81,29 @@ def find_elm(root, elm):
             return step + path
     return None
 
+def errpath(filename, tree, elm):
+    """
+    Return the XML class path of the element
+    """
+    if elm is None:
+        return ""
+    path = ""
+    if 'class' in elm.attrib:
+        path += elm.attrib['class']
+    oid = elm.attrib.get('id')
+    if oid is not None:
+        oid = oid.encode('ascii','ignore').decode('ascii')
+        path += "[@id='%s']" % oid
+    if lxml:
+        elm = elm.getparent()
+        while elm is not None:
+            step = step_elm(elm)
+            path = step + path
+            elm = elm.getparent()
+    else:
+        path = find_elm(tree.getroot(), elm)[:-1]
+    path = filename + ':' + path
+    return path
 
 def elm_prefix(filename, elm):
     """
@@ -99,10 +128,44 @@ def elm_name(elm):
         return name
     return ""
 
-def err(filename, tree, elm, msg):
-    global errors
+def elm_suppr(filename, tree, elm, msgtype):
+    """
+    Return the prefix to be displayed to the user and the suppression line for
+    the warning type "msgtype" for element "elm"
+    """
+    global gen_suppr, gen_supprfile, pflag
+
+    if suppressions or gen_suppr is not None or pflag:
+        prefix = errpath(filename, tree, elm)
+
+    if suppressions or gen_suppr is not None:
+        suppr = '%s %s' % (prefix, msgtype)
+
+        if gen_suppr is not None and msgtype is not None:
+            if gen_supprfile is None:
+                gen_supprfile = open(gen_suppr, 'w')
+            print(suppr, file=gen_supprfile)
+    else:
+        suppr = None
+
+    if not pflag:
+        # Use user-friendly line numbers
+        prefix = elm_prefix(filename, elm)
 
-    prefix = elm_prefix(filename, elm)
+    return (prefix, suppr)
+
+def err(filename, tree, elm, msgtype, msg):
+    """
+    Emit an error for an element
+    """
+    global errors, errexists
+
+    (prefix, suppr) = elm_suppr(filename, tree, elm, msgtype)
+
+    if suppr in suppressions:
+        # Suppressed
+        errexists += 1
+        return
 
     errors += 1
     msg = "%s ERROR: %s%s" % (prefix, elm_name(elm), msg)
@@ -111,13 +174,23 @@ def err(filename, tree, elm, msg):
         print(msg, file=outfile)
 
 
-def warn(filename, elm, msg):
-    global Werror, Wnone, errors, warnings
+def warn(filename, tree, elm, msgtype, msg):
+    """
+    Emit a warning for an element
+    """
+    global Werror, Wnone, errors, errexists, warnings, warnexists
 
     if Wnone:
         return
 
-    prefix = elm_prefix(filename, elm)
+    (prefix, suppr) = elm_suppr(filename, tree, elm, msgtype)
+    if suppr in suppressions:
+        # Suppressed
+        if Werror:
+            errexists += 1
+        else:
+            warnexists += 1
+        return
 
     if Werror:
         errors += 1
@@ -136,9 +209,9 @@ def check_objects(filename, tree, elm, objects, target):
     """
     length = len(list(objects))
     if length == 0:
-        err(filename, tree, elm, "uses undeclared target '%s'" % target)
+        err(filename, tree, elm, "undeclared-target", "uses undeclared target '%s'" % target)
     elif length > 1:
-        err(filename, tree, elm, "several targets are named '%s'" % target)
+        err(filename, tree, elm, "multiple-target", "several targets are named '%s'" % target)
 
 def check_props(filename, tree, root, elm, props):
     """
@@ -220,25 +293,53 @@ def check_a11y_relation(filename, tree):
         if len(labelled_by) > 0:
             continue
 
+        # Case 3/4: has an ID...
+        oid = obj.attrib.get('id')
+        if oid is not None:
+            # ...referenced by a single "label-for" <relation>
+            rels = root.iterfind(".//relation[@target='%s']" % oid)
+            labelfor = [r for r in rels if r.attrib.get('type') == 'label-for']
+            if len(labelfor) == 1:
+                continue
+            if len(labelfor) > 1:
+                err(filename, tree, obj, "multiple-label-for", "has too many elements"
+                    ", expected single <relation type='label-for' target='%s'>"
+                    "%s" % (oid, elm_lines(labelfor)))
+                continue
+
+            # ...referenced by a single "mnemonic_widget"
+            props = root.iterfind(".//property[@name='mnemonic_widget']")
+            props = [p for p in props if p.text == oid]
+            # TODO: warn when more than one.
+            if len(props) >= 1:
+                continue
+
         # TODO: after a few more checks and false-positives filtering, warn
         # that this does not have a label
+        if obj.attrib['class'] == "GtkScale":
+            # GtkScale definitely needs a context
+            err(filename, tree, obj, "no-labelled-by", "has no accessibility label")
 
 
 def usage():
-    print("%s [-W error|none] [-o LOG_FILE] [file ... | -L filelist]" % progname,
+    print("%s [-W error|none] [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-o LOG_FILE] [file ... | -L filelist]" % progname,
           file=sys.stderr)
+    print("  -p print XML class path instead of line number");
+    print("  -g Generate suppression file SUPPR_FILE");
+    print("  -s Suppress warnings given by file SUPPR_FILE");
     print("  -o Also prints errors and warnings to given file");
     sys.exit(2)
 
 
 def main():
-    global Werror, Wnone, errors, outfile
+    global pflag, Werror, Wnone, gen_suppr, gen_supprfile, suppressions, errors, outfile
 
     try:
-        opts, args = getopt.getopt(sys.argv[1:], "W:o:L:")
+        opts, args = getopt.getopt(sys.argv[1:], "W:pg:s:o:L:")
     except getopt.GetoptError:
         usage()
 
+    suppr = None
     out = None
     filelist = None
     for o, a in opts:
@@ -247,11 +348,28 @@ def main():
                 Werror = True
             elif a == "none":
                 Wnone = True
+        elif o == "-p":
+            pflag = True
+        elif o == "-g":
+            gen_suppr = a
+        elif o == "-s":
+            suppr = a
         elif o == "-o":
             out = a
         elif o == "-L":
             filelist = a
 
+    # Read suppression file before overwriting it
+    if suppr is not None:
+        try:
+            supprfile = open(suppr, 'r')
+            for line in supprfile.readlines():
+                prefix = line.rstrip()
+                suppressions[prefix] = True
+            supprfile.close()
+        except IOError:
+            pass
+
     if out is not None:
         outfile = open(out, 'w')
 
@@ -270,10 +388,10 @@ def main():
         try:
             tree = ET.parse(filename)
         except ET.ParseError:
-            err(filename, None, None, "malformatted xml file")
+            err(filename, None, None, "parse", "malformatted xml file")
             continue
         except IOError:
-            err(filename, None, None, "unable to read file")
+            err(filename, None, None, None, "unable to read file")
             continue
 
         try:
@@ -281,11 +399,26 @@ def main():
         except Exception as error:
             import traceback
             traceback.print_exc()
-            err(filename, None, None, "error parsing file")
-
+            err(filename, None, None, "parse", "error parsing file")
+
+    if errors > 0 or errexists > 0:
+        estr = "%s new error%s" % (errors, 's' if errors > 1 else '')
+        if errexists > 0:
+            estr += " (%s suppressed by %s)" % (errexists, suppr)
+        print(estr)
+
+    if warnings > 0 or warnexists > 0:
+        wstr = "%s new warning%s" % (warnings,
+                                           's' if warnings > 1 else '')
+        if warnexists > 0:
+            wstr += " (%s suppressed by %s)" % (warnexists, suppr)
+        print(wstr)
+
+    if gen_supprfile is not None:
+        gen_supprfile.close()
     if outfile is not None:
         outfile.close()
-    if errors > 0:
+    if errors > 0 and gen_suppr is None:
         sys.exit(1)
 
 
diff --git a/solenv/gbuild/UIConfig.mk b/solenv/gbuild/UIConfig.mk
index 0de62d712009..a0d8974fffe9 100644
--- a/solenv/gbuild/UIConfig.mk
+++ b/solenv/gbuild/UIConfig.mk
@@ -124,6 +124,11 @@ $(call gb_UIConfig_get_clean_target,%) :
 	$(call gb_Output_announce,$*,$(false),UIA,2)
 	rm -f $(call gb_UIConfig_get_a11yerrors_target,$*)
 
+# Enable this to regenerate suppression files
+ifeq (1,0)
+GEN_A11Y_SUPPRS = -g $(UI_A11YSUPPRS)
+endif
+
 define gb_UIConfig_a11yerrors__command
 $(call gb_Output_announce,$(2),$(true),UIA,1)
 $(call gb_UIConfig__gla11y_command)
@@ -154,6 +159,7 @@ $(call gb_PackageSet_add_package,$(call gb_UIConfig_get_packagesetname,$(1)),$(c
 $(call gb_UIConfig_get_target,$(1)) :| $(dir $(call gb_UIConfig_get_target,$(1))).dir
 $(call gb_UIConfig_get_imagelist_target,$(1)) :| $(dir $(call gb_UIConfig_get_imagelist_target,$(1))).dir
 $(call gb_UIConfig_get_a11yerrors_target,$(1)) :| $(dir $(call gb_UIConfig_get_a11yerrors_target,$(1))).dir
+$(call gb_UIConfig_get_a11yerrors_target,$(1)) : UI_A11YSUPPRS := $(SRCDIR)/solenv/sanitizers/ui/$(1).suppr
 $(call gb_UIConfig_get_target,$(1)) : $(call gb_PackageSet_get_target,$(call gb_UIConfig_get_packagesetname,$(1)))
 $(call gb_UIConfig_get_clean_target,$(1)) : $(call gb_PackageSet_get_clean_target,$(call gb_UIConfig_get_packagesetname,$(1)))
 
diff --git a/solenv/gbuild/platform/com_GCC_class.mk b/solenv/gbuild/platform/com_GCC_class.mk
index 2d4c2cf90a98..3345c7f6cca3 100644
--- a/solenv/gbuild/platform/com_GCC_class.mk
+++ b/solenv/gbuild/platform/com_GCC_class.mk
@@ -159,7 +159,7 @@ define gb_UIConfig__gla11y_command
 $(call gb_Helper_abbreviate_dirs,\
 	$(gb_UIConfig_LXML_PATH) $(gb_Helper_set_ld_path) \
 	$(call gb_ExternalExecutable_get_command,python) \
-	$(gb_UIConfig_gla11y_SCRIPT) -o $@ $(UIFILES)
+	$(gb_UIConfig_gla11y_SCRIPT) -s $(UI_A11YSUPPRS) $(GEN_A11Y_SUPPRS) -o $@ $(UIFILES)
 )
 
 endef
diff --git a/solenv/gbuild/platform/com_MSC_class.mk b/solenv/gbuild/platform/com_MSC_class.mk
index 271459635a3c..467d27cbc529 100644
--- a/solenv/gbuild/platform/com_MSC_class.mk
+++ b/solenv/gbuild/platform/com_MSC_class.mk
@@ -583,7 +583,7 @@ $(call gb_Helper_abbreviate_dirs,\
 	FILES=$(call var2file,$(shell $(gb_MKTEMP)),100,$(UIFILES)) && \
 	$(gb_UIConfig_LXML_PATH) $(gb_Helper_set_ld_path) \
 	$(call gb_ExternalExecutable_get_command,python) \
-	$(gb_UIConfig_gla11y_SCRIPT) -o $@ -L $$FILES
+	$(gb_UIConfig_gla11y_SCRIPT) -s $(UI_A11YSUPPRS) $(GEN_A11Y_SUPPRS) -o $@ -L $$FILES
 )
 
 endef
diff --git a/solenv/sanitizers/ui/cui.suppr b/solenv/sanitizers/ui/cui.suppr
new file mode 100644
index 000000000000..d9e0294f464f
--- /dev/null
+++ b/solenv/sanitizers/ui/cui.suppr
@@ -0,0 +1,3 @@
+cui/uiconfig/ui/gradientpage.ui:GtkBox[@id='GradientPage']/GtkFrame[@id='frame1']/GtkAlignment[@id='alignment1']/GtkBox[@id='box2']/GtkGrid[@id='grid6']/GtkScale[@id='incrementslider'] no-labelled-by
+cui/uiconfig/ui/gradientpage.ui:GtkBox[@id='GradientPage']/GtkFrame[@id='frame1']/GtkAlignment[@id='alignment1']/GtkBox[@id='box2']/GtkGrid[@id='grid3']/GtkScale[@id='borderslider'] no-labelled-by
+cui/uiconfig/ui/hatchpage.ui:GtkBox[@id='HatchPage']/GtkFrame[@id='frame1']/GtkAlignment[@id='alignment1']/GtkBox[@id='box3']/GtkBox[@id='box1']/GtkScale[@id='angleslider'] no-labelled-by
diff --git a/solenv/sanitizers/ui/svt.suppr b/solenv/sanitizers/ui/svt.suppr
new file mode 100644
index 000000000000..40ba68b7b93a
--- /dev/null
+++ b/solenv/sanitizers/ui/svt.suppr
@@ -0,0 +1,2 @@
+svtools/uiconfig/ui/graphicexport.ui:GtkDialog[@id='GraphicExportDialog']/GtkBox[@id='dialog-vbox1']/GtkBox[@id='box1']/GtkFrame[@id='jpgquality']/GtkAlignment[@id='alignment5']/GtkGrid[@id='grid2']/GtkScale[@id='compressionjpgsb'] no-labelled-by
+svtools/uiconfig/ui/graphicexport.ui:GtkDialog[@id='GraphicExportDialog']/GtkBox[@id='dialog-vbox1']/GtkBox[@id='box1']/GtkFrame[@id='pngcompression']/GtkAlignment[@id='alignment13']/GtkGrid[@id='grid9']/GtkScale[@id='compressionpngsb'] no-labelled-by
diff --git a/solenv/sanitizers/ui/svx.suppr b/solenv/sanitizers/ui/svx.suppr
new file mode 100644
index 000000000000..9179abb2dbd5
--- /dev/null
+++ b/solenv/sanitizers/ui/svx.suppr
@@ -0,0 +1,6 @@
+svx/uiconfig/ui/compressgraphicdialog.ui:GtkDialog[@id='CompressGraphicDialog']/GtkBox[@id='dialog-vbox1']/GtkGrid/GtkFrame[@id='frame2']/GtkAlignment[@id='alignment1']/GtkGrid[@id='grid2']/GtkAlignment/GtkGrid/GtkScale[@id='scale-quality'] no-labelled-by
+svx/uiconfig/ui/compressgraphicdialog.ui:GtkDialog[@id='CompressGraphicDialog']/GtkBox[@id='dialog-vbox1']/GtkGrid/GtkFrame[@id='frame2']/GtkAlignment[@id='alignment1']/GtkGrid[@id='grid2']/GtkAlignment/GtkGrid/GtkScale[@id='scale-compression'] no-labelled-by
+svx/uiconfig/ui/mediaplayback.ui:GtkGrid[@id='MediaPlaybackPanel']/GtkGrid[@id='grid1']/GtkScale[@id='timeslider'] no-labelled-by
+svx/uiconfig/ui/mediaplayback.ui:GtkGrid[@id='MediaPlaybackPanel']/GtkGrid[@id='grid1']/GtkScale[@id='volumeslider'] no-labelled-by
+svx/uiconfig/ui/sidebararea.ui:GtkGrid[@id='AreaPropertyPanel']/GtkBox[@id='box1']/GtkGrid[@id='grid1']/GtkScale[@id='transparencyslider'] no-labelled-by
+svx/uiconfig/ui/sidebarshadow.ui:GtkGrid[@id='ShadowPropertyPanel']/GtkGrid[@id='grid3']/GtkBox[@id='box2']/GtkBox[@id='box1']/GtkGrid[@id='grid2']/GtkScale[@id='transparency_slider'] no-labelled-by


More information about the Libreoffice-commits mailing list