| #!/usr/bin/env python |
| # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*- |
| # |
| # This file is part of the LibreOffice project. |
| # |
| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| # |
| # This file incorporates work covered by the following license notice: |
| # |
| # Copyright (c) 2018 Martin Pieuchot |
| # Copyright (c) 2018-2020 Samuel Thibault <sthibault@hypra.fr> |
| # |
| # Permission to use, copy, modify, and distribute this software for any |
| # purpose with or without fee is hereby granted, provided that the above |
| # copyright notice and this permission notice appear in all copies. |
| # |
| # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| |
| # Take LibreOffice (glade) .ui files and check for non accessible widgets |
| |
| # A white paper documents the rationale of the implementation: |
| # |
| # https://inria.hal.science/hal-02957129 |
| |
| from __future__ import print_function |
| |
| import os |
| import sys |
| import getopt |
| try: |
| import lxml.etree as ET |
| lxml = True |
| except ImportError: |
| if sys.version_info < (2,7): |
| print("gla11y needs lxml or python >= 2.7") |
| exit() |
| import xml.etree.ElementTree as ET |
| lxml = False |
| |
| howto_url = "https://wiki.documentfoundation.org/Development/Accessibility" |
| |
| # Toplevel widgets |
| widgets_toplevel = [ |
| 'GtkWindow', |
| 'GtkOffscreenWindow', |
| 'GtkApplicationWindow', |
| 'GtkDialog', |
| 'GtkFileChooserDialog', |
| 'GtkColorChooserDialog', |
| 'GtkFontChooserDialog', |
| 'GtkMessageDialog', |
| 'GtkRecentChooserDialog', |
| 'GtkAssistant', |
| 'GtkAppChooserDialog', |
| 'GtkPrintUnixDialog', |
| 'GtkShortcutsWindow', |
| ] |
| |
| widgets_ignored = widgets_toplevel + [ |
| # Containers |
| 'GtkBox', |
| 'GtkGrid', |
| 'GtkNotebook', |
| 'GtkFrame', |
| 'GtkAspectFrame', |
| 'GtkListBox', |
| 'GtkFlowBox', |
| 'GtkOverlay', |
| 'GtkMenuBar', |
| 'GtkToolbar', |
| 'GtkToolpalette', |
| 'GtkPaned', |
| 'GtkHPaned', |
| 'GtkVPaned', |
| 'GtkButtonBox', |
| 'GtkHButtonBox', |
| 'GtkVButtonBox', |
| 'GtkLayout', |
| 'GtkFixed', |
| 'GtkEventBox', |
| 'GtkExpander', |
| 'GtkViewport', |
| 'GtkScrolledWindow', |
| 'GtkRevealer', |
| 'GtkSearchBar', |
| 'GtkHeaderBar', |
| 'GtkStack', |
| 'GtkPopover', |
| 'GtkPopoverMenu', |
| 'GtkActionBar', |
| 'GtkHandleBox', |
| 'GtkShortcutsSection', |
| 'GtkShortcutsGroup', |
| 'GtkTable', |
| |
| 'GtkVBox', |
| 'GtkHBox', |
| 'GtkToolItem', |
| 'GtkMenu', |
| |
| # Invisible actions |
| 'GtkSeparator', |
| 'GtkHSeparator', |
| 'GtkVSeparator', |
| 'GtkAction', |
| 'GtkToggleAction', |
| 'GtkActionGroup', |
| 'GtkCellRendererGraph', |
| 'GtkCellRendererPixbuf', |
| 'GtkCellRendererProgress', |
| 'GtkCellRendererSpin', |
| 'GtkCellRendererText', |
| 'GtkCellRendererToggle', |
| 'GtkSeparatorMenuItem', |
| 'GtkSeparatorToolItem', |
| |
| # Storage objects |
| 'GtkListStore', |
| 'GtkTreeStore', |
| 'GtkTreeModelFilter', |
| 'GtkTreeModelSort', |
| |
| 'GtkEntryBuffer', |
| 'GtkTextBuffer', |
| 'GtkTextTag', |
| 'GtkTextTagTable', |
| |
| 'GtkSizeGroup', |
| 'GtkWindowGroup', |
| 'GtkAccelGroup', |
| 'GtkAdjustment', |
| 'GtkEntryCompletion', |
| 'GtkIconFactory', |
| 'GtkStatusIcon', |
| 'GtkFileFilter', |
| 'GtkRecentFilter', |
| 'GtkRecentManager', |
| 'GThemedIcon', |
| |
| 'GtkTreeSelection', |
| |
| 'GtkListBoxRow', |
| 'GtkTreeViewColumn', |
| |
| # Useless to label |
| 'GtkScrollbar', |
| 'GtkHScrollbar', |
| 'GtkStatusbar', |
| 'GtkInfoBar', |
| |
| # These are actually labels |
| 'GtkLinkButton', |
| |
| # This precisely give a11y information :) |
| 'AtkObject', |
| ] |
| |
| widgets_suffixignored = [ |
| ] |
| |
| # These widgets always need a label |
| widgets_needlabel = [ |
| 'GtkEntry', |
| 'GtkSearchEntry', |
| 'GtkScale', |
| 'GtkHScale', |
| 'GtkVScale', |
| 'GtkSpinButton', |
| 'GtkSwitch', |
| ] |
| |
| # These widgets normally have their own label |
| widgets_buttons = [ |
| 'GtkButton', |
| 'GtkToolButton', |
| 'GtkToggleButton', |
| 'GtkToggleToolButton', |
| 'GtkRadioButton', |
| 'GtkRadioToolButton', |
| 'GtkCheckButton', |
| 'GtkModelButton', |
| 'GtkLockButton', |
| 'GtkColorButton', |
| 'GtkMenuButton', |
| |
| 'GtkMenuItem', |
| 'GtkImageMenuItem', |
| 'GtkMenuToolButton', |
| 'GtkRadioMenuItem', |
| 'GtkCheckMenuItem', |
| ] |
| |
| # These widgets are labels that can label other widgets |
| widgets_labels = [ |
| 'GtkLabel', |
| 'GtkAccelLabel', |
| ] |
| |
| # The rest should probably be labelled if there are orphan labels |
| |
| # GtkSpinner |
| # GtkProgressBar |
| # GtkLevelBar |
| |
| # GtkComboBox |
| # GtkComboBoxText |
| # GtkFileChooserButton |
| # GtkAppChooserButton |
| # GtkFontButton |
| # GtkCalendar |
| # GtkColorChooserWidget |
| |
| # GtkCellView |
| # GtkTreeView |
| # GtkTextView |
| # GtkIconView |
| |
| # GtkImage |
| # GtkArrow |
| # GtkDrawingArea |
| |
| # GtkScaleButton |
| # GtkVolumeButton |
| |
| |
| # TODO: |
| # GtkColorPlane ? |
| # GtkColorScale ? |
| # GtkColorSwatch ? |
| # GtkFileChooserWidget ? |
| # GtkFishbowl ? |
| # GtkFontChooserWidget ? |
| # GtkIcon ? |
| # GtkInspector* ? |
| # GtkMagnifier ? |
| # GtkPathBar ? |
| # GtkPlacesSidebar ? |
| # GtkPlacesView ? |
| # GtkPrinterOptionWidget ? |
| # GtkStackCombo ? |
| # GtkStackSidebar ? |
| # GtkStackSwitcher ? |
| |
| progname = os.path.basename(sys.argv[0]) |
| |
| # This dictionary contains the set of suppression lines as read from the |
| # suppression file(s). It is merely indexed by the text of the suppression line |
| # and contains whether the suppressions was unused. |
| suppressions = {} |
| |
| # This dictionary is indexed like suppressions and returns a "file:line" string |
| # to report where in the suppression file the suppression was read |
| suppressions_to_line = {} |
| |
| # This dictionary is similar to the suppressions dictionary, but for false |
| # positives rather than suppressions |
| false_positives = {} |
| |
| # This dictionary is indexed by the xml id and returns the element object. |
| ids = {} |
| # This dictionary is indexed by the xml id and returns whether several objects |
| # have the same id. |
| ids_dup = {} |
| |
| # This dictionary is indexed by the xml id of an element A and returns the list |
| # of objects which are labelled-by A. |
| labelled_by_elm = {} |
| |
| # This dictionary is indexed by the xml id of an element A and returns the list |
| # of objects which are label-for A. |
| label_for_elm = {} |
| |
| # This dictionary is indexed by the xml id of an element A and returns the list |
| # of objects which have a mnemonic-for A. |
| mnemonic_for_elm = {} |
| |
| # Possibly a file name to put generated suppression lines in |
| gen_suppr = None |
| # The corresponding opened file |
| gen_supprfile = None |
| # A prefix to remove from file names in the generated suppression lines |
| suppr_prefix = "" |
| |
| # Possibly an opened file in which our output should also be written to. |
| outfile = None |
| |
| # Whether -p option was set, i.e. print XML class path instead of line number in |
| # the output |
| pflag = False |
| |
| # Whether we should warn about labels which are orphan |
| warn_orphan_labels = True |
| |
| # Number of errors |
| errors = 0 |
| # Number of suppressed errors |
| errexists = 0 |
| # Number of warnings |
| warnings = 0 |
| # Number of suppressed warnings |
| warnexists = 0 |
| # Number of fatal errors |
| fatals = 0 |
| # Number of suppressed fatal errors |
| fatalexists = 0 |
| |
| # List of warnings and errors which are fatal |
| # |
| # Format of each element: (enabled, type, class) |
| # See the is_enabled function: the list is traversed completely, each element |
| # can specify whether it enables or disables the warning, possibly the type of |
| # warning to be enabled/disabled, possibly the class of XML element for which it |
| # should be enabled. |
| # |
| # This mechanism matches the semantic of the parameters on the command line, |
| # each of which refining the semantic set by the previous parameters |
| dofatals = [ ] |
| |
| # List of warnings and errors which are enabled |
| # Same format as dofatals |
| enables = [ ] |
| |
| # buffers all printed output, so it isn't split in parallel builds |
| output_buffer = "" |
| |
| # |
| # XML browsing and printing functions |
| # |
| |
| def elm_parent(root, elm): |
| """ |
| Return the parent of the element. |
| """ |
| if lxml: |
| return elm.getparent() |
| else: |
| def find_parent(cur, elm): |
| for o in cur: |
| if o == elm: |
| return cur |
| parent = find_parent(o, elm) |
| if parent is not None: |
| return parent |
| return None |
| return find_parent(root, 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): |
| """ |
| 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 |
| |
| 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 = "//" + path + "[@id='%s']" % oid |
| else: |
| 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 |
| |
| # |
| # Warning/Error printing functions |
| # |
| |
| 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 |
| if not name: |
| name = "'" + elm.tag + "'" |
| if lxml: |
| name += " line " + str(elm.sourceline) |
| return name |
| return "" |
| |
| def elm_name_line(elm): |
| """ |
| Return a display name of the element with line number |
| """ |
| if elm is not None: |
| name = elm_name(elm) |
| if lxml and " line " not in name: |
| name += "line " + str(elm.sourceline) + " " |
| return name |
| return "" |
| |
| def elm_line(elm): |
| """ |
| Return the line for the given element. |
| """ |
| if lxml: |
| return " line " + str(elm.sourceline) |
| else: |
| return "" |
| |
| 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 elms_names_lines(elms): |
| """ |
| Return the list of names and lines for the given elements. |
| """ |
| return ', '.join([elm_name_line(elm) for elm in elms]) |
| |
| def elm_suppr(filename, tree, elm, msgtype, dogen): |
| """ |
| 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, suppr_prefix, pflag |
| |
| if suppressions or false_positives or gen_suppr is not None or pflag: |
| prefix = errpath(filename, tree, elm) |
| if prefix[0:len(suppr_prefix)] == suppr_prefix: |
| prefix = prefix[len(suppr_prefix):] |
| |
| if suppressions or false_positives or gen_suppr is not None: |
| suppr = '%s %s' % (prefix, msgtype) |
| |
| if gen_suppr is not None and msgtype is not None and dogen: |
| 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) |
| if prefix[0:len(suppr_prefix)] == suppr_prefix: |
| prefix = prefix[len(suppr_prefix):] |
| |
| return (prefix, suppr) |
| |
| def is_enabled(elm, msgtype, l, default): |
| """ |
| Test whether warning type msgtype is enabled for elm in l |
| """ |
| enabled = default |
| for (enable, thetype, klass) in l: |
| # Match warning type |
| if thetype is not None: |
| if thetype != msgtype: |
| continue |
| # Match elm class |
| if klass is not None and elm is not None: |
| if klass != elm.attrib.get('class'): |
| continue |
| enabled = enable |
| return enabled |
| |
| def err(filename, tree, elm, msgtype, msg, error = True): |
| """ |
| Emit a warning or error for an element |
| """ |
| global errors, errexists, warnings, warnexists, fatals, fatalexists, output_buffer |
| |
| # Let user tune whether a warning or error |
| fatal = is_enabled(elm, msgtype, dofatals, error) |
| |
| # By default warnings and errors are enabled, but let user tune it |
| if not is_enabled(elm, msgtype, enables, True): |
| return |
| |
| (prefix, suppr) = elm_suppr(filename, tree, elm, msgtype, True) |
| if suppr in false_positives: |
| # That was actually expected |
| return |
| if suppr in suppressions: |
| # Suppressed |
| suppressions[suppr] = False |
| if fatal: |
| fatalexists += 1 |
| if error: |
| errexists += 1 |
| else: |
| warnexists += 1 |
| return |
| |
| if error: |
| errors += 1 |
| else: |
| warnings += 1 |
| if fatal: |
| fatals += 1 |
| |
| msg = "%s %s%s: %s%s" % (prefix, |
| "FATAL " if fatal else "", |
| "ERROR" if error else "WARNING", |
| elm_name(elm), msg) |
| output_buffer += msg + "\n" |
| if outfile is not None: |
| print(msg, file=outfile) |
| |
| def warn(filename, tree, elm, msgtype, msg): |
| """ |
| Emit a warning for an element |
| """ |
| err(filename, tree, elm, msgtype, msg, False) |
| |
| # |
| # Labelling testing functions |
| # |
| |
| def find_button_parent(root, elm): |
| """ |
| Find a parent which is a button |
| """ |
| if lxml: |
| parent = elm.getparent() |
| if parent is not None: |
| if parent.attrib.get('class') in widgets_buttons: |
| return parent |
| return find_button_parent(root, parent) |
| else: |
| def find_parent(cur, elm): |
| for o in cur: |
| if o == elm: |
| if cur.attrib.get('class') in widgets_buttons: |
| # we are the button, immediately above the target |
| return cur |
| else: |
| # we aren't the button, but target is over there |
| return True |
| parent = find_parent(o, elm) |
| if parent == True: |
| # It is over there, but didn't find a button yet |
| if cur.attrib.get('class') in widgets_buttons: |
| # we are the button |
| return cur |
| else: |
| return True |
| if parent is not None: |
| # we have the button parent over there |
| return parent |
| return None |
| parent = find_parent(root, elm) |
| if parent == True: |
| parent = None |
| return parent |
| |
| |
| def is_labelled_parent(elm): |
| """ |
| Return whether this element is a labelled parent |
| """ |
| klass = elm.attrib.get('class') |
| if klass in widgets_toplevel: |
| return True |
| if klass == 'GtkShortcutsGroup': |
| children = elm.findall("property[@name='title']") |
| if len(children) >= 1: |
| return True |
| if klass == 'GtkFrame' or klass == 'GtkNotebook': |
| children = elm.findall("child[@type='tab']") + elm.findall("child[@type='label']") |
| if len(children) >= 1: |
| return True |
| return False |
| |
| def elm_labelled_parent(root, elm): |
| """ |
| Return the first labelled parent of the element, which can thus be used as |
| the root of widgets with common labelled context |
| """ |
| |
| if lxml: |
| def find_labelled_parent(elm): |
| if is_labelled_parent(elm): |
| return elm |
| parent = elm.getparent() |
| if parent is None: |
| return None |
| return find_labelled_parent(parent) |
| parent = elm.getparent() |
| if parent is None: |
| return None |
| return find_labelled_parent(elm.getparent()) |
| else: |
| def find_labelled_parent(cur, elm): |
| if cur == elm: |
| # the target element is over there |
| return True |
| for o in cur: |
| parent = find_labelled_parent(o, elm) |
| if parent == True: |
| # target element is over there, check ourself |
| if is_labelled_parent(cur): |
| # yes, and we are the first ancestor of the target element |
| return cur |
| else: |
| # no, but target element is over there. |
| return True |
| if parent != None: |
| # the first ancestor of the target element was over there |
| return parent |
| return None |
| parent = find_labelled_parent(root, elm) |
| if parent == True: |
| parent = None |
| return parent |
| |
| def is_orphan_label(filename, tree, root, obj, orphan_root, doprint = False): |
| """ |
| Check whether this label has no accessibility relation, or doubtful relation |
| because another label labels the same target |
| """ |
| global label_for_elm, labelled_by_elm, mnemonic_for_elm, warnexists |
| |
| # label-for |
| label_for = obj.findall("accessibility/relation[@type='label-for']") |
| for rel in label_for: |
| target = rel.attrib['target'] |
| l = label_for_elm[target] |
| if len(l) > 1: |
| return True |
| |
| # mnemonic_widget |
| mnemonic_for = obj.findall("property[@name='mnemonic_widget']") + \ |
| obj.findall("property[@name='mnemonic-widget']") |
| for rel in mnemonic_for: |
| target = rel.text |
| l = mnemonic_for_elm[target] |
| if len(l) > 1: |
| return True |
| |
| if len(label_for) > 0: |
| # At least one label-for, we are not orphan. |
| return False |
| |
| if len(mnemonic_for) > 0: |
| # At least one mnemonic_widget, we are not orphan. |
| return False |
| |
| labelled_by = obj.findall("accessibility/relation[@type='labelled-by']") |
| if len(labelled_by) > 0: |
| # Oh, a labelled label, probably not to be labelling anything |
| return False |
| |
| # explicit role? |
| roles = [x.text for x in obj.findall("child[@internal-child='accessible']/object[@class='AtkObject']/property[@name='AtkObject::accessible-role']")] |
| roles += [x.attrib.get("type") for x in obj.findall("accessibility/role")] |
| if len(roles) > 1 and doprint: |
| err(filename, tree, obj, "multiple-role", "has multiple <child internal-child='accessible'><object class='AtkObject'><property name='AtkBoject::accessible-role'>" |
| "%s" % elms_lines(children)) |
| for role in roles: |
| if role == 'static' or role == 'ATK_ROLE_STATIC': |
| # This is static text, not meant to label anything |
| return False |
| |
| parent = elm_parent(root, obj) |
| if parent is not None: |
| childtype = parent.attrib.get('type') |
| if childtype is None: |
| childtype = parent.attrib.get('internal-child') |
| if parent.tag == 'child' and childtype == 'label' \ |
| or childtype == 'tab': |
| # This is a frame or a notebook label, not orphan. |
| return False |
| |
| if find_button_parent(root, obj) is not None: |
| # This label is part of a button |
| return False |
| |
| oid = obj.attrib.get('id') |
| if oid is not None: |
| if oid in labelled_by_elm: |
| # Some widget is labelled by us, we are not orphan. |
| # We should have had a label-for, will warn about it later. |
| return False |
| |
| # No label-for, no mnemonic-for, no labelled-by, we are orphan. |
| (_, suppr) = elm_suppr(filename, tree, obj, "orphan-label", False) |
| if suppr in false_positives: |
| # That was actually expected |
| return False |
| if suppr in suppressions: |
| # Warning suppressed for this label |
| if suppressions[suppr]: |
| warnexists += 1 |
| suppressions[suppr] = False |
| return False |
| |
| if doprint: |
| context = elm_name(orphan_root) |
| if context: |
| context = " within " + context |
| warn(filename, tree, obj, "orphan-label", "does not specify what it labels" + context) |
| return True |
| |
| def is_orphan_widget(filename, tree, root, obj, orphan, orphan_root, doprint = False): |
| """ |
| Check whether this widget has no accessibility relation. |
| """ |
| global warnexists |
| if obj.tag != 'object': |
| return False |
| |
| oid = obj.attrib.get('id') |
| klass = obj.attrib.get('class') |
| |
| # "Don't care" special case |
| if klass in widgets_ignored: |
| return False |
| for suffix in widgets_suffixignored: |
| if klass[-len(suffix):] == suffix: |
| return False |
| |
| # Widgets usual do not strictly require a label, i.e. a labelled parent |
| # is enough for context, but some do always need one. |
| requires_label = klass in widgets_needlabel |
| |
| labelled_by = obj.findall("accessibility/relation[@type='labelled-by']") |
| |
| # Labels special case |
| if klass in widgets_labels: |
| return False |
| |
| # Case 1: has an explicit <child internal-child="accessible"> sub-element |
| children = obj.findall("child[@internal-child='accessible']") |
| if len(children) > 1 and doprint: |
| err(filename, tree, obj, "multiple-accessible", "has multiple <child internal-child='accessible'>" |
| "%s" % elms_lines(children)) |
| if len(children) >= 1: |
| return False |
| |
| # Case 2: has an <accessibility> sub-element with a "labelled-by" |
| # <relation> pointing to an existing element. |
| if len(labelled_by) > 0: |
| return False |
| |
| # Case 3: has a label-for |
| if oid in label_for_elm: |
| return False |
| |
| # Case 4: has a mnemonic |
| if oid in mnemonic_for_elm: |
| return False |
| |
| # Case 5: Has a <property name="tooltip_text"> |
| tooltips = obj.findall("property[@name='tooltip_text']") + \ |
| obj.findall("property[@name='tooltip-text']") |
| if len(tooltips) > 1 and doprint: |
| err(filename, tree, obj, "multiple-tooltip", "has multiple tooltip_text properties") |
| if len(tooltips) >= 1 and klass != 'GtkCheckButton': |
| return False |
| |
| # Case 6: Has a <property name="placeholder_text"> |
| placeholders = obj.findall("property[@name='placeholder_text']") + \ |
| obj.findall("property[@name='placeholder-text']") |
| if len(placeholders) > 1 and doprint: |
| err(filename, tree, obj, "multiple-placeholder", "has multiple placeholder_text properties") |
| if len(placeholders) >= 1: |
| return False |
| |
| # Buttons usually don't need an external label, their own is enough, (but they do need one) |
| if klass in widgets_buttons: |
| |
| labels = obj.findall("property[@name='label']") |
| if len(labels) > 1 and doprint: |
| err(filename, tree, obj, "multiple-label", "has multiple label properties") |
| if len(labels) >= 1: |
| # Has a <property name="label"> |
| return False |
| |
| actions = obj.findall("property[@name='action_name']") |
| if len(actions) > 1 and doprint: |
| err(filename, tree, obj, "multiple-action_name", "has multiple action_name properties") |
| if len(actions) >= 1: |
| # Has a <property name="action_name"> |
| return False |
| |
| # Uses id as an action_name |
| if 'id' in obj.attrib: |
| if obj.attrib['id'].startswith(".uno:"): |
| return False |
| |
| gtklabels = obj.findall(".//object[@class='GtkLabel']") + obj.findall(".//object[@class='GtkAccelLabel']") |
| if len(gtklabels) >= 1: |
| # Has a custom label |
| return False |
| |
| # no label for a button, warn |
| if doprint: |
| warn(filename, tree, obj, "button-no-label", "does not have its own label") |
| if not is_enabled(obj, "button-no-label", enables, True): |
| # Warnings disabled |
| return False |
| (_, suppr) = elm_suppr(filename, tree, obj, "button-no-label", False) |
| if suppr in false_positives: |
| # That was actually expected |
| return False |
| if suppr in suppressions: |
| # Warning suppressed for this widget |
| if suppressions[suppr]: |
| warnexists += 1 |
| suppressions[suppr] = False |
| return False |
| return True |
| |
| # GtkImages special case |
| if klass == "GtkImage": |
| uses = [u for u in tree.iterfind(".//object/property[@name='image']") if u.text == oid] |
| if len(uses) > 0: |
| # This image is just used by another element, don't warn |
| # about the image itself, we probably want the warning on |
| # the element instead. |
| return False |
| |
| if find_button_parent(root, obj) is not None: |
| # This image is part of a button, we want the warning on the button |
| # instead, if any. |
| return False |
| |
| # GtkEntry special case |
| if klass == 'GtkEntry' or klass == 'GtkSearchEntry': |
| parent = elm_parent(root, obj) |
| if parent is not None: |
| if parent.tag == 'child' and \ |
| parent.attrib.get('internal-child') == "entry": |
| # This is an internal entry of another widget. Relations |
| # will be handled by that widget. |
| return False |
| |
| # GtkShortcutsShortcut special case |
| if klass == 'GtkShortcutsShortcut': |
| children = obj.findall("property[@name='title']") |
| if len(children) >= 1: |
| return False |
| |
| # Really no label, perhaps emit a warning |
| if not is_enabled(obj, "no-labelled-by", enables, True): |
| # Warnings disabled for this class of widgets |
| return False |
| (_, suppr) = elm_suppr(filename, tree, obj, "no-labelled-by", False) |
| if suppr in false_positives: |
| # That was actually expected |
| return False |
| if suppr in suppressions: |
| # Warning suppressed for this widget |
| if suppressions[suppr]: |
| warnexists += 1 |
| suppressions[suppr] = False |
| return False |
| |
| if not orphan: |
| # No orphan label, so probably the labelled parent provides enough |
| # context. |
| if requires_label: |
| # But these always need a label. |
| if doprint: |
| warn(filename, tree, obj, "no-labelled-by", "has no accessibility label") |
| return True |
| return False |
| |
| if doprint: |
| context = elm_name(orphan_root) |
| if context: |
| context = " within " + context |
| warn(filename, tree, obj, "no-labelled-by", "has no accessibility label while there are orphan labels" + context) |
| return True |
| |
| def orphan_items(filename, tree, root, elm): |
| """ |
| Check whether from some element there exists orphan labels and orphan widgets |
| """ |
| orphan_labels = False |
| orphan_widgets = False |
| if elm.attrib.get('class') in widgets_labels: |
| orphan_labels = is_orphan_label(filename, tree, root, elm, None) |
| else: |
| orphan_widgets = is_orphan_widget(filename, tree, root, elm, True, None) |
| for obj in elm: |
| # We are not interested in orphan labels under another labelled |
| # parent. This also allows to keep linear complexity. |
| if not is_labelled_parent(obj): |
| label, widget = orphan_items(filename, tree, root, obj) |
| if label: |
| orphan_labels = True |
| if widget: |
| orphan_widgets = True |
| if orphan_labels and orphan_widgets: |
| # No need to look up more |
| break |
| return orphan_labels, orphan_widgets |
| |
| # |
| # UI accessibility checks |
| # |
| |
| def check_props(filename, tree, root, elm, forward): |
| """ |
| Check the given list of relation properties |
| """ |
| props = elm.findall("property[@name='" + forward + "']") |
| for prop in props: |
| if prop.text not in ids: |
| err(filename, tree, elm, "undeclared-target", forward + " uses undeclared target '%s'" % prop.text) |
| return props |
| |
| def is_visible(obj): |
| visible = False |
| visible_prop = obj.findall("property[@name='visible']") |
| visible_len = len(visible_prop) |
| if visible_len: |
| visible_txt = visible_prop[visible_len - 1].text |
| if visible_txt.lower() == "true": |
| visible = True |
| elif visible_txt.lower() == "false": |
| visible = False |
| return visible |
| |
| def check_rels(filename, tree, root, elm, forward, backward = None): |
| """ |
| Check the relations given by forward |
| """ |
| oid = elm.attrib.get('id') |
| rels = elm.findall("accessibility/relation[@type='" + forward + "']") |
| for rel in rels: |
| target = rel.attrib['target'] |
| if target not in ids: |
| err(filename, tree, elm, "undeclared-target", forward + " uses undeclared target '%s'" % target) |
| elif backward is not None: |
| widget = ids[target] |
| backrels = widget.findall("accessibility/relation[@type='" + backward + "']") |
| if len([x for x in backrels if x.attrib['target'] == oid]) == 0: |
| err(filename, tree, elm, "missing-" + backward, "has " + forward + \ |
| ", but is not " + backward + " by " + elm_name_line(widget)) |
| return rels |
| |
| 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. |
| """ |
| global widgets_ignored, ids, label_for_elm, labelled_by_elm, mnemonic_for_elm |
| |
| def check_elm(orphan_root, obj, orphan_labels, orphan_widgets): |
| """ |
| Check one element, knowing that orphan_labels/widgets tell whether |
| there are orphan labels and widgets within orphan_root |
| """ |
| |
| oid = obj.attrib.get('id') |
| klass = obj.attrib.get('class') |
| |
| # "Don't care" special case |
| if klass in widgets_ignored: |
| return |
| for suffix in widgets_suffixignored: |
| if klass[-len(suffix):] == suffix: |
| return |
| |
| # Widgets usual do not strictly require a label, i.e. a labelled parent |
| # is enough for context, but some do always need one. |
| requires_label = klass in widgets_needlabel |
| |
| if oid is not None: |
| # Check that ids are unique |
| if oid in ids_dup: |
| if ids[oid] == obj: |
| # We are the first, warn |
| duplicates = tree.findall(".//object[@id='" + oid + "']") |
| err(filename, tree, obj, "duplicate-id", "has the same id as other elements " + elms_names_lines(duplicates)) |
| |
| # Check label-for and their dual labelled-by |
| label_for = check_rels(filename, tree, root, obj, "label-for", "labelled-by") |
| |
| # Check labelled-by and its dual label-for |
| labelled_by = check_rels(filename, tree, root, obj, "labelled-by", "label-for") |
| |
| visible = is_visible(obj) |
| |
| # warning message type "syntax" used: |
| # |
| # multiple-* => 2+ XML tags of the inspected element itself |
| # duplicate-* => 2+ XML tags of other elements referencing this element |
| |
| # Should have only one label |
| if len(labelled_by) >= 1: |
| if oid in mnemonic_for_elm: |
| warn(filename, tree, obj, "labelled-by-and-mnemonic", |
| "has both a mnemonic " + elm_name_line(mnemonic_for_elm[oid][0]) + "and labelled-by relation") |
| if len(labelled_by) > 1: |
| warn(filename, tree, obj, "multiple-labelled-by", "has multiple labelled-by relations") |
| |
| if oid in labelled_by_elm: |
| if len(labelled_by_elm[oid]) == 1: |
| paired = labelled_by_elm[oid][0] |
| if paired != None and visible != is_visible(paired): |
| warn(filename, tree, obj, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired)) |
| |
| if oid in label_for_elm: |
| if len(label_for_elm[oid]) > 1: |
| warn(filename, tree, obj, "duplicate-label-for", "is referenced by multiple label-for " + elms_names_lines(label_for_elm[oid])) |
| elif len(label_for_elm[oid]) == 1: |
| paired = label_for_elm[oid][0] |
| if visible != is_visible(paired): |
| warn(filename, tree, obj, "visibility-conflict", "visibility conflicts with paired " + elm_name_line(paired)) |
| |
| if oid in mnemonic_for_elm: |
| if len(mnemonic_for_elm[oid]) > 1: |
| warn(filename, tree, obj, "duplicate-mnemonic", "is referenced by multiple mnemonic_widget " + elms_names_lines(mnemonic_for_elm[oid])) |
| |
| # Check controlled-by/controller-for |
| controlled_by = check_rels(filename, tree, root, obj, "controlled-by", "controller-for") |
| controller_for = check_rels(filename, tree, root, obj, "controlled-for", "controlled-by") |
| |
| # Labels special case |
| if klass in widgets_labels: |
| properties = check_props(filename, tree, root, obj, "mnemonic_widget") + \ |
| check_props(filename, tree, root, obj, "mnemonic-widget") |
| if len(properties) > 1: |
| err(filename, tree, obj, "multiple-mnemonic", "has multiple mnemonic_widgets properties" |
| "%s" % elms_lines(properties)) |
| |
| # Emit orphaning warnings |
| if warn_orphan_labels or orphan_widgets: |
| is_orphan_label(filename, tree, root, obj, orphan_root, True) |
| |
| # We are done with the label |
| return |
| |
| # Not a label, will perhaps need one |
| |
| # Emit orphaning warnings |
| is_orphan_widget(filename, tree, root, obj, orphan_labels, orphan_root, True) |
| |
| root = tree.getroot() |
| |
| # Flush ids and relations from previous files |
| ids = {} |
| ids_dup = {} |
| labelled_by_elm = {} |
| label_for_elm = {} |
| mnemonic_for_elm = {} |
| |
| # First pass to get links into hash tables, no warning, just record duplicates |
| for obj in root.iter('object'): |
| oid = obj.attrib.get('id') |
| if oid is not None: |
| if oid not in ids: |
| ids[oid] = obj |
| else: |
| ids_dup[oid] = True |
| |
| labelled_by = obj.findall("accessibility/relation[@type='labelled-by']") |
| for rel in labelled_by: |
| target = rel.attrib.get('target') |
| if target is not None: |
| if target not in labelled_by_elm: |
| labelled_by_elm[target] = [ obj ] |
| else: |
| labelled_by_elm[target].append(obj) |
| |
| label_for = obj.findall("accessibility/relation[@type='label-for']") |
| for rel in label_for: |
| target = rel.attrib.get('target') |
| if target is not None: |
| if target not in label_for_elm: |
| label_for_elm[target] = [ obj ] |
| else: |
| label_for_elm[target].append(obj) |
| |
| mnemonic_for = obj.findall("property[@name='mnemonic_widget']") + \ |
| obj.findall("property[@name='mnemonic-widget']") |
| for rel in mnemonic_for: |
| target = rel.text |
| if target is not None: |
| if target not in mnemonic_for_elm: |
| mnemonic_for_elm[target] = [ obj ] |
| else: |
| mnemonic_for_elm[target].append(obj) |
| |
| # Second pass, recursive depth-first, to be able to efficiently know whether |
| # there are orphan labels within a part of the tree. |
| def recurse(orphan_root, obj, orphan_labels, orphan_widgets): |
| if obj == root or is_labelled_parent(obj): |
| orphan_root = obj |
| orphan_labels, orphan_widgets = orphan_items(filename, tree, root, obj) |
| |
| if obj.tag == 'object': |
| check_elm(orphan_root, obj, orphan_labels, orphan_widgets) |
| |
| for o in obj: |
| recurse(orphan_root, o, orphan_labels, orphan_widgets) |
| |
| recurse(root, root, False, False) |
| |
| # |
| # Main |
| # |
| |
| def usage(fatal = True): |
| print("`%s' checks accessibility of glade .ui files" % progname) |
| print("") |
| print("Usage: %s [-p] [-g SUPPR_FILE] [-s SUPPR_FILE] [-f SUPPR_FILE] [-P PREFIX] [-o LOG_FILE] [file ...]" % progname) |
| print("") |
| 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, but count them") |
| print(" -f Suppress warnings given by file SUPPR_FILE completely") |
| print(" -P Remove PREFIX from file names in warnings") |
| print(" -o Also prints errors and warnings to given file") |
| print("") |
| print(" --widgets-FOO [+][CLASS1[,CLASS2[,...]]]") |
| print(" Give or extend one of the lists of widget classes, where FOO can be:") |
| print(" - toplevel : widgets to be considered toplevel windows") |
| print(" - ignored : widgets which do not need labelling (e.g. GtkBox)") |
| print(" - suffixignored : suffixes of widget classes which do not need labelling") |
| print(" - needlabel : widgets which always need labelling (e.g. GtkEntry)") |
| print(" - buttons : widgets which need their own label but not more") |
| print(" (e.g. GtkButton)") |
| print(" - labels : widgets which provide labels (e.g. GtkLabel)") |
| print(" --widgets-print print default widgets lists") |
| print("") |
| print(" --enable-all enable all warnings/dofatals (default)") |
| print(" --disable-all disable all warnings/dofatals") |
| print(" --fatal-all make all warnings dofatals") |
| print(" --not-fatal-all do not make all warnings dofatals (default)") |
| print("") |
| print(" --enable-type=TYPE enable warning/fatal type TYPE") |
| print(" --disable-type=TYPE disable warning/fatal type TYPE") |
| print(" --fatal-type=TYPE make warning type TYPE a fatal") |
| print(" --not-fatal-type=TYPE make warning type TYPE not a fatal") |
| print("") |
| print(" --enable-widgets=CLASS enable warning/fatal type CLASS") |
| print(" --disable-widgets=CLASS disable warning/fatal type CLASS") |
| print(" --fatal-widgets=CLASS make warning type CLASS a fatal") |
| print(" --not-fatal-widgets=CLASS make warning type CLASS not a fatal") |
| print("") |
| print(" --enable-specific=TYPE.CLASS enable warning/fatal type TYPE for widget") |
| print(" class CLASS") |
| print(" --disable-specific=TYPE.CLASS disable warning/fatal type TYPE for widget") |
| print(" class CLASS") |
| print(" --fatal-specific=TYPE.CLASS make warning type TYPE a fatal for widget") |
| print(" class CLASS") |
| print(" --not-fatal-specific=TYPE.CLASS make warning type TYPE not a fatal for widget") |
| print(" class CLASS") |
| print("") |
| print(" --disable-orphan-labels only warn about orphan labels when there are") |
| print(" orphan widgets in the same context") |
| print("") |
| print("Report bugs to <bugs@hypra.fr>") |
| sys.exit(2 if fatal else 0) |
| |
| def widgets_opt(widgets_list, arg): |
| """ |
| Replace or extend `widgets_list' with the list of classes contained in `arg' |
| """ |
| append = arg and arg[0] == '+' |
| if append: |
| arg = arg[1:] |
| |
| if arg: |
| widgets = arg.split(',') |
| else: |
| widgets = [] |
| |
| if not append: |
| del widgets_list[:] |
| |
| widgets_list.extend(widgets) |
| |
| |
| def main(): |
| global pflag, gen_suppr, gen_supprfile, suppressions, suppr_prefix, false_positives, dofatals, enables, dofatals, warn_orphan_labels |
| global widgets_toplevel, widgets_ignored, widgets_suffixignored, widgets_needlabel, widgets_buttons, widgets_labels |
| global outfile, output_buffer |
| |
| try: |
| opts, args = getopt.getopt(sys.argv[1:], "hpiIg:s:f:P:o:L:", [ |
| "help", |
| "version", |
| |
| "widgets-toplevel=", |
| "widgets-ignored=", |
| "widgets-suffixignored=", |
| "widgets-needlabel=", |
| "widgets-buttons=", |
| "widgets-labels=", |
| "widgets-print", |
| |
| "enable-all", |
| "disable-all", |
| "fatal-all", |
| "not-fatal-all", |
| |
| "enable-type=", |
| "disable-type=", |
| "fatal-type=", |
| "not-fatal-type=", |
| |
| "enable-widgets=", |
| "disable-widgets=", |
| "fatal-widgets=", |
| "not-fatal-widgets=", |
| |
| "enable-specific=", |
| "disable-specific=", |
| "fatal-specific=", |
| "not-fatal-specific=", |
| |
| "disable-orphan-labels", |
| ] ) |
| except getopt.GetoptError: |
| usage() |
| |
| suppr = None |
| false = None |
| out = None |
| filelist = None |
| |
| for o, a in opts: |
| if o == "--help" or o == "-h": |
| usage(False) |
| if o == "--version": |
| print("0.1") |
| sys.exit(0) |
| elif o == "-p": |
| pflag = True |
| elif o == "-g": |
| gen_suppr = a |
| elif o == "-s": |
| suppr = a |
| elif o == "-f": |
| false = a |
| elif o == "-P": |
| suppr_prefix = a |
| elif o == "-o": |
| out = a |
| elif o == "-L": |
| filelist = a |
| |
| elif o == "--widgets-toplevel": |
| widgets_opt(widgets_toplevel, a) |
| elif o == "--widgets-ignored": |
| widgets_opt(widgets_ignored, a) |
| elif o == "--widgets-suffixignored": |
| widgets_opt(widgets_suffixignored, a) |
| elif o == "--widgets-needlabel": |
| widgets_opt(widgets_needlabel, a) |
| elif o == "--widgets-buttons": |
| widgets_opt(widgets_buttons, a) |
| elif o == "--widgets-labels": |
| widgets_opt(widgets_labels, a) |
| elif o == "--widgets-print": |
| print("--widgets-toplevel '" + ','.join(widgets_toplevel) + "'") |
| print("--widgets-ignored '" + ','.join(widgets_ignored) + "'") |
| print("--widgets-suffixignored '" + ','.join(widgets_suffixignored) + "'") |
| print("--widgets-needlabel '" + ','.join(widgets_needlabel) + "'") |
| print("--widgets-buttons '" + ','.join(widgets_buttons) + "'") |
| print("--widgets-labels '" + ','.join(widgets_labels) + "'") |
| sys.exit(0) |
| |
| elif o == '--enable-all': |
| enables.append( (True, None, None) ) |
| elif o == '--disable-all': |
| enables.append( (False, None, None) ) |
| elif o == '--fatal-all': |
| dofatals.append( (True, None, None) ) |
| elif o == '--not-fatal-all': |
| dofatals.append( (False, None, None) ) |
| |
| elif o == '--enable-type': |
| enables.append( (True, a, None) ) |
| elif o == '--disable-type': |
| enables.append( (False, a, None) ) |
| elif o == '--fatal-type': |
| dofatals.append( (True, a, None) ) |
| elif o == '--not-fatal-type': |
| dofatals.append( (False, a, None) ) |
| |
| elif o == '--enable-widgets': |
| enables.append( (True, None, a) ) |
| elif o == '--disable-widgets': |
| enables.append( (False, None, a) ) |
| elif o == '--fatal-widgets': |
| dofatals.append( (True, None, a) ) |
| elif o == '--not-fatal-widgets': |
| dofatals.append( (False, None, a) ) |
| |
| elif o == '--enable-specific': |
| (thetype, klass) = a.split('.', 1) |
| enables.append( (True, thetype, klass) ) |
| elif o == '--disable-specific': |
| (thetype, klass) = a.split('.', 1) |
| enables.append( (False, thetype, klass) ) |
| elif o == '--fatal-specific': |
| (thetype, klass) = a.split('.', 1) |
| dofatals.append( (True, thetype, klass) ) |
| elif o == '--not-fatal-specific': |
| (thetype, klass) = a.split('.', 1) |
| dofatals.append( (False, thetype, klass) ) |
| |
| elif o == '--disable-orphan-labels': |
| warn_orphan_labels = False |
| |
| output_header = "" |
| |
| # Read suppression file before overwriting it |
| if suppr is not None: |
| try: |
| output_header += "Suppression file: " + suppr + "\n" |
| supprfile = open(suppr, 'r') |
| line_no = 0 |
| for line in supprfile.readlines(): |
| line_no = line_no + 1 |
| if line.startswith('#'): |
| continue |
| prefix = line.rstrip() |
| suppressions[prefix] = True |
| suppressions_to_line[prefix] = "%s:%u" % (suppr, line_no) |
| supprfile.close() |
| except IOError: |
| pass |
| |
| # Read false positives file |
| if false is not None: |
| try: |
| output_header += "False positive file: " + false + "\n" |
| falsefile = open(false, 'r') |
| for line in falsefile.readlines(): |
| if line.startswith('#'): |
| continue |
| prefix = line.rstrip() |
| false_positives[prefix] = True |
| falsefile.close() |
| except IOError: |
| pass |
| |
| 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, None, "parse", "malformatted xml file") |
| continue |
| except IOError: |
| err(filename, None, None, None, "unable to read file") |
| continue |
| |
| try: |
| check_a11y_relation(filename, tree) |
| except Exception as error: |
| import traceback |
| output_buffer += traceback.format_exc() |
| err(filename, None, None, "parse", "error parsing file") |
| |
| if errors > 0 or errexists > 0: |
| output_buffer += "%s new error%s" % (errors, 's' if errors != 1 else '') |
| if errexists > 0: |
| output_buffer += " (%s suppressed by %s, please fix %s)" % (errexists, suppr, 'them' if errexists > 1 else 'it') |
| output_buffer += "\n" |
| |
| if warnings > 0 or warnexists > 0: |
| output_buffer += "%s new warning%s" % (warnings, 's' if warnings != 1 else '') |
| if warnexists > 0: |
| output_buffer += " (%s suppressed by %s, please fix %s)" % (warnexists, suppr, 'them' if warnexists > 1 else 'it') |
| output_buffer += "\n" |
| |
| if fatals > 0 or fatalexists > 0: |
| output_buffer += "%s new fatal%s" % (fatals, 's' if fatals != 1 else '') |
| if fatalexists > 0: |
| output_buffer += " (%s suppressed by %s, please fix %s)" % (fatalexists, suppr, 'them' if fatalexists > 1 else 'it') |
| output_buffer += "\n" |
| |
| n = 0 |
| for (suppr,unused) in suppressions.items(): |
| if unused: |
| n += 1 |
| |
| if n > 0: |
| output_buffer += "%s suppression%s unused:\n" % (n, 's' if n != 1 else '') |
| for (suppr,unused) in suppressions.items(): |
| if unused: |
| output_buffer += " %s:%s\n" % (suppressions_to_line[suppr], suppr) |
| |
| if gen_supprfile is not None: |
| gen_supprfile.close() |
| if outfile is not None: |
| outfile.close() |
| |
| if gen_suppr is None: |
| if output_buffer != "": |
| output_buffer += "Explanations are available on " + howto_url + "\n" |
| |
| if fatals > 0: |
| print(output_header.rstrip() + "\n" + output_buffer) |
| sys.exit(1) |
| |
| if len(output_buffer) > 0: |
| print(output_header.rstrip() + "\n" + output_buffer) |
| |
| if __name__ == "__main__": |
| try: |
| main() |
| except KeyboardInterrupt: |
| pass |
| |
| # vim: set shiftwidth=4 softtabstop=4 expandtab: |