34fd462ac5
Change-Id: I0d60cb6b3ee2105d58ea56f3efc5bb5ee856a44a Reviewed-on: https://gerrit.libreoffice.org/c/core/+/151147 Reviewed-by: Caolán McNamara <caolan.mcnamara@collabora.com> Tested-by: Caolán McNamara <caolan.mcnamara@collabora.com>
1485 lines
48 KiB
Python
Executable file
1485 lines
48 KiB
Python
Executable file
#!/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:
|