From 96654f888204e3de0e03dcefea0a0c8efa15698a Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Wed, 24 Nov 2021 18:03:17 +0100 Subject: [PATCH 01/20] provide more context for lint context line number and xpath --- lib/galaxy/tool_util/lint.py | 81 ++++++++++++++------ lib/galaxy/tool_util/linters/citations.py | 12 +-- lib/galaxy/tool_util/linters/command.py | 12 +-- lib/galaxy/tool_util/linters/general.py | 35 +++++---- lib/galaxy/tool_util/linters/help.py | 15 ++-- lib/galaxy/tool_util/linters/inputs.py | 93 ++++++++++++----------- lib/galaxy/tool_util/linters/outputs.py | 31 ++++---- lib/galaxy/tool_util/linters/stdio.py | 18 +++-- lib/galaxy/tool_util/linters/tests.py | 33 +++++--- 9 files changed, 197 insertions(+), 133 deletions(-) diff --git a/lib/galaxy/tool_util/lint.py b/lib/galaxy/tool_util/lint.py index c747da489450..003856cdf112 100644 --- a/lib/galaxy/tool_util/lint.py +++ b/lib/galaxy/tool_util/lint.py @@ -63,6 +63,20 @@ def lint_xml_with(lint_context, tool_xml, extra_modules=None): return lint_tool_source_with(lint_context, tool_source, extra_modules=extra_modules) +class LintMessage: + def __init__(self, level, message, line, xpath=None): + self.level = level + self.message = message + self.line = line + self.xpath = xpath + + def __str__(self): + rval = f".. {self.level.upper()}: {self.message}" + if self.line is not None: + rval += f" (line {self.line})" + return rval + + # TODO: Nothing inherently tool-y about LintContext and in fact # it is reused for repositories in planemo. Therefore, it should probably # be moved to galaxy.util.lint. @@ -80,10 +94,9 @@ def lint(self, name, lint_func, lint_target): if name in self.skip_types: return self.printed_linter_info = False - self.valid_messages = [] - self.info_messages = [] - self.warn_messages = [] - self.error_messages = [] + self.message_list = [] + + # call linter lint_func(lint_target, self) # TODO: colorful emoji if in click CLI. if self.error_messages: @@ -99,41 +112,65 @@ def print_linter_info(): self.printed_linter_info = True print(f"Applying linter {name}... {status}") - for message in self.error_messages: + for message in self.message_list: + if message.level != "error": + continue self.found_errors = True print_linter_info() - print(f".. ERROR: {message}") + print(f"{message}") if self.level != LEVEL_ERROR: - for message in self.warn_messages: + for message in self.message_list: + if message.level != "warning": + continue self.found_warns = True print_linter_info() - print(f".. WARNING: {message}") + print(f"{message}") if self.level == LEVEL_ALL: - for message in self.info_messages: + for message in self.message_list: + if message.level != "info": + continue print_linter_info() - print(f".. INFO: {message}") - for message in self.valid_messages: + print(f"{message}") + for message in self.message_list: + if message.level != "check": + continue print_linter_info() - print(f".. CHECK: {message}") + print(f"{message}") + + @property + def valid_messages(self): + return [x.message for x in self.message_list if x.level == "check"] + + @property + def info_messages(self): + return [x.message for x in self.message_list if x.level == "info"] + + @property + def warn_messages(self): + return [x.message for x in self.message_list if x.level == "warning"] + + @property + def error_messages(self): + return [x.message for x in self.message_list if x.level == "error"] - def __handle_message(self, message_list, message, *args): + def __handle_message(self, level, message, line, xpath, *args): if args: message = message % args - message_list.append(message) + self.message_list.append(LintMessage(level=level, message=message, line=line, xpath=xpath)) - def valid(self, message, *args): - self.__handle_message(self.valid_messages, message, *args) + def valid(self, message, line=None, xpath=None, *args): + self.__handle_message("check", message, line, xpath, *args) - def info(self, message, *args): - self.__handle_message(self.info_messages, message, *args) + def info(self, message, line=None, xpath=None, *args): + self.__handle_message("info", message, line, xpath, *args) - def error(self, message, *args): - self.__handle_message(self.error_messages, message, *args) + def error(self, message, line=None, xpath=None, *args): + self.__handle_message("error", message, line, xpath, *args) - def warn(self, message, *args): - self.__handle_message(self.warn_messages, message, *args) + def warn(self, message, line=None, xpath=None, *args): + self.__handle_message("warning", message, line, xpath, *args) def failed(self, fail_level): found_warns = self.found_warns diff --git a/lib/galaxy/tool_util/linters/citations.py b/lib/galaxy/tool_util/linters/citations.py index c038b4fdd0e2..60f3527a2238 100644 --- a/lib/galaxy/tool_util/linters/citations.py +++ b/lib/galaxy/tool_util/linters/citations.py @@ -10,26 +10,26 @@ def lint_citations(tool_xml, lint_ctx): root = tool_xml.getroot() citations = root.findall("citations") if len(citations) > 1: - lint_ctx.error("More than one citation section found, behavior undefined.") + lint_ctx.error("More than one citation section found, behavior undefined.", line=citations[1].sourceline) return if len(citations) == 0: - lint_ctx.warn("No citations found, consider adding citations to your tool.") + lint_ctx.warn("No citations found, consider adding citations to your tool.", line=root.sourceline) return valid_citations = 0 for citation in citations[0]: if citation.tag != "citation": - lint_ctx.warn(f"Unknown tag discovered in citations block [{citation.tag}], will be ignored.") + lint_ctx.warn(f"Unknown tag discovered in citations block [{citation.tag}], will be ignored.", line=citation.sourceline) continue citation_type = citation.attrib.get("type") if citation_type not in ('bibtex', 'doi'): - lint_ctx.warn("Unknown citation type discovered [%s], will be ignored.", citation_type) + lint_ctx.warn(f"Unknown citation type discovered [{citation_type}], will be ignored.", line=citation.sourceline) continue if citation.text is None or not citation.text.strip(): - lint_ctx.error(f'Empty {citation_type} citation.') + lint_ctx.error(f'Empty {citation_type} citation.', line=citation.sourceline) continue valid_citations += 1 if valid_citations > 0: - lint_ctx.valid("Found %d likely valid citations.", valid_citations) + lint_ctx.valid(f"Found {valid_citations} likely valid citations.", line=root.sourceline) diff --git a/lib/galaxy/tool_util/linters/command.py b/lib/galaxy/tool_util/linters/command.py index e675f92cc4ab..cd0afe744eee 100644 --- a/lib/galaxy/tool_util/linters/command.py +++ b/lib/galaxy/tool_util/linters/command.py @@ -10,16 +10,16 @@ def lint_command(tool_xml, lint_ctx): root = tool_xml.getroot() commands = root.findall("command") if len(commands) > 1: - lint_ctx.error("More than one command tag found, behavior undefined.") + lint_ctx.error("More than one command tag found, behavior undefined.", line=commands[1].sourceline) return if len(commands) == 0: - lint_ctx.error("No command tag found, must specify a command template to execute.") + lint_ctx.error("No command tag found, must specify a command template to execute.", line=root.sourceline) return command = get_command(tool_xml) if "TODO" in command: - lint_ctx.warn("Command template contains TODO text.") + lint_ctx.warn("Command template contains TODO text.", line=command.sourceline) command_attrib = command.attrib interpreter_type = None @@ -29,14 +29,14 @@ def lint_command(tool_xml, lint_ctx): elif key == "detect_errors": detect_errors = value if detect_errors not in ["default", "exit_code", "aggressive"]: - lint_ctx.warn(f"Unknown detect_errors attribute [{detect_errors}]") + lint_ctx.warn(f"Unknown detect_errors attribute [{detect_errors}]", line=command.sourceline) interpreter_info = "" if interpreter_type: interpreter_info = f" with interpreter of type [{interpreter_type}]" if interpreter_type: - lint_ctx.info("Command uses deprecated 'interpreter' attribute.") - lint_ctx.info(f"Tool contains a command{interpreter_info}.") + lint_ctx.info("Command uses deprecated 'interpreter' attribute.", line=command.sourceline) + lint_ctx.info(f"Tool contains a command{interpreter_info}.", line=command.sourceline) def get_command(tool_xml): diff --git a/lib/galaxy/tool_util/linters/general.py b/lib/galaxy/tool_util/linters/general.py index 5d47bd6670a1..59d0bf4b14b2 100644 --- a/lib/galaxy/tool_util/linters/general.py +++ b/lib/galaxy/tool_util/linters/general.py @@ -27,39 +27,47 @@ def lint_general(tool_source, lint_ctx): """Check tool version, name, and id.""" + # determine line to report for general problems with outputs + tool_xml = getattr(tool_source, "xml_tree", None) + try: + tool_line = tool_xml.find("./tool").sourceline + except: + tool_line = 0 version = tool_source.parse_version() or '' parsed_version = packaging.version.parse(version) if not version: - lint_ctx.error(ERROR_VERSION_MSG) + lint_ctx.error(ERROR_VERSION_MSG, line=tool_line) elif isinstance(parsed_version, packaging.version.LegacyVersion): - lint_ctx.warn(WARN_VERSION_MSG % version) + lint_ctx.warn(WARN_VERSION_MSG % version, line=tool_line) elif version != version.strip(): - lint_ctx.warn(WARN_WHITESPACE_MSG % ('Tool version', version)) + lint_ctx.warn(WARN_WHITESPACE_MSG % ('Tool version', version), line=tool_line) else: - lint_ctx.valid(VALID_VERSION_MSG % version) + lint_ctx.valid(VALID_VERSION_MSG % version, line=tool_line) name = tool_source.parse_name() if not name: - lint_ctx.error(ERROR_NAME_MSG) + lint_ctx.error(ERROR_NAME_MSG, line=tool_line) elif name != name.strip(): - lint_ctx.warn(WARN_WHITESPACE_MSG % ('Tool name', name)) + lint_ctx.warn(WARN_WHITESPACE_MSG % ('Tool name', name), line=tool_line) else: - lint_ctx.valid(VALID_NAME_MSG % name) + lint_ctx.valid(VALID_NAME_MSG % name, line=tool_line) tool_id = tool_source.parse_id() if not tool_id: - lint_ctx.error(ERROR_ID_MSG) + lint_ctx.error(ERROR_ID_MSG, line=tool_line) else: - lint_ctx.valid(VALID_ID_MSG % tool_id) + lint_ctx.valid(VALID_ID_MSG % tool_id, line=tool_line) + if re.search(r"\s", tool_id): + lint_ctx.warn(WARN_ID_WHITESPACE_MSG % tool_id, line=tool_line) profile = tool_source.parse_profile() profile_valid = PROFILE_PATTERN.match(profile) is not None if not profile_valid: - lint_ctx.error(PROFILE_INVALID_MSG) + lint_ctx.error(PROFILE_INVALID_MSG, line=tool_line) elif profile == "16.01": - lint_ctx.valid(PROFILE_INFO_DEFAULT_MSG) + lint_ctx.valid(PROFILE_INFO_DEFAULT_MSG, line=tool_line) else: - lint_ctx.valid(PROFILE_INFO_SPECIFIED_MSG % profile) + lint_ctx.valid(PROFILE_INFO_SPECIFIED_MSG % profile, line=tool_line) requirements, containers = tool_source.parse_requirements_and_containers() for r in requirements: @@ -72,6 +80,3 @@ def lint_general(tool_source, lint_ctx): elif r.version != r.version.strip(): lint_ctx.warn( WARN_WHITESPACE_MSG % ('Requirement version', r.version)) - - if re.search(r"\s", tool_id): - lint_ctx.warn(WARN_ID_WHITESPACE_MSG % tool_id) diff --git a/lib/galaxy/tool_util/linters/help.py b/lib/galaxy/tool_util/linters/help.py index df0a3f9b801e..be9f8ecce46b 100644 --- a/lib/galaxy/tool_util/linters/help.py +++ b/lib/galaxy/tool_util/linters/help.py @@ -7,31 +7,32 @@ def lint_help(tool_xml, lint_ctx): """Ensure tool contains exactly one valid RST help block.""" + # determine line to report for general problems with outputs root = tool_xml.getroot() helps = root.findall("help") if len(helps) > 1: - lint_ctx.error("More than one help section found, behavior undefined.") + lint_ctx.error("More than one help section found, behavior undefined.", line=helps[1].sourceline) return if len(helps) == 0: - lint_ctx.warn("No help section found, consider adding a help section to your tool.") + lint_ctx.warn("No help section found, consider adding a help section to your tool.", line=root.sourceline) return help = helps[0].text or '' if not help.strip(): - lint_ctx.warn("Help section appears to be empty.") + lint_ctx.warn("Help section appears to be empty.", line=helps[0].sourceline) return - lint_ctx.valid("Tool contains help section.") + lint_ctx.valid("Tool contains help section.", line=root.sourceline) invalid_rst = rst_invalid(help) if "TODO" in help: - lint_ctx.warn("Help contains TODO text.") + lint_ctx.warn("Help contains TODO text.", line=helps[0].sourceline) if invalid_rst: - lint_ctx.warn(f"Invalid reStructuredText found in help - [{invalid_rst}].") + lint_ctx.warn(f"Invalid reStructuredText found in help - [{invalid_rst}].", line=helps[0].sourceline) else: - lint_ctx.valid("Help contains valid reStructuredText.") + lint_ctx.valid("Help contains valid reStructuredText.", line=helps[0].sourceline) def rst_invalid(text): diff --git a/lib/galaxy/tool_util/linters/inputs.py b/lib/galaxy/tool_util/linters/inputs.py index 7c8a47c69af7..b7cd16ad3981 100644 --- a/lib/galaxy/tool_util/linters/inputs.py +++ b/lib/galaxy/tool_util/linters/inputs.py @@ -46,6 +46,11 @@ def lint_inputs(tool_xml, lint_ctx): """Lint parameters in a tool's inputs block.""" + # determine line to report for general problems with outputs + try: + tool_line = tool_xml.find("./tool").sourceline + except: + tool_line = 0 datasource = is_datasource(tool_xml) inputs = tool_xml.findall("./inputs//param") num_inputs = 0 @@ -53,7 +58,7 @@ def lint_inputs(tool_xml, lint_ctx): num_inputs += 1 param_attrib = param.attrib if "name" not in param_attrib and "argument" not in param_attrib: - lint_ctx.error("Found param input with no name specified.") + lint_ctx.error("Found param input with no name specified.", line=param.sourceline) continue param_name = _parse_name(param_attrib.get("name"), param_attrib.get("argument")) if "name" in param_attrib and "argument" in param_attrib: @@ -61,16 +66,16 @@ def lint_inputs(tool_xml, lint_ctx): lint_ctx.warn(f"Param input [{param_name}] 'name' attribute is redundant if argument implies the same name.") if "type" not in param_attrib: - lint_ctx.error(f"Param input [{param_name}] input with no type specified.") + lint_ctx.error(f"Param input [{param_name}] input with no type specified.", line=param.sourceline) continue param_type = param_attrib["type"] if not is_valid_cheetah_placeholder(param_name): - lint_ctx.warn(f"Param input [{param_name}] is not a valid Cheetah placeholder.") + lint_ctx.warn(f"Param input [{param_name}] is not a valid Cheetah placeholder.", line=param.sourceline) if param_type == "data": if "format" not in param_attrib: - lint_ctx.warn(f"Param input [{param_name}] with no format specified - 'data' format will be assumed.") + lint_ctx.warn(f"Param input [{param_name}] with no format specified - 'data' format will be assumed.", line=param.sourceline) elif param_type == "select": # get dynamic/statically defined options dynamic_options = param.get("dynamic_options", None) @@ -79,11 +84,11 @@ def lint_inputs(tool_xml, lint_ctx): select_options = param.findall('./option') if dynamic_options is not None: - lint_ctx.warn(f"Select parameter [{param_name}] uses deprecated 'dynamic_options' attribute.") + lint_ctx.warn(f"Select parameter [{param_name}] uses deprecated 'dynamic_options' attribute.", line=param.sourceline) # check if options are defined by exactly one possibility if (dynamic_options is not None) + (len(options) > 0) + (len(select_options) > 0) != 1: - lint_ctx.error(f"Select parameter [{param_name}] options have to be defined by either 'option' children elements, a 'options' element or the 'dynamic_options' attribute.") + lint_ctx.error(f"Select parameter [{param_name}] options have to be defined by either 'option' children elements, a 'options' element or the 'dynamic_options' attribute.", line=param.sourceline) # lint dynamic options if len(options) == 1: @@ -93,10 +98,10 @@ def lint_inputs(tool_xml, lint_ctx): for f in filters: ftype = f.get("type", None) if ftype is None: - lint_ctx.error(f"Select parameter [{param_name}] contains filter without type.") + lint_ctx.error(f"Select parameter [{param_name}] contains filter without type.", line=f.sourceline) continue if ftype not in FILTER_TYPES: - lint_ctx.error(f"Select parameter [{param_name}] contains filter with unknown type '{ftype}'.") + lint_ctx.error(f"Select parameter [{param_name}] contains filter with unknown type '{ftype}'.", line=f.sourceline) continue if ftype in ['add_value', 'data_meta']: filter_adds_options = True @@ -109,33 +114,33 @@ def lint_inputs(tool_xml, lint_ctx): if (from_file is None and from_parameter is None and from_dataset is None and from_data_table is None and not filter_adds_options): - lint_ctx.error(f"Select parameter [{param_name}] options tag defines no options. Use 'from_dataset', 'from_data_table', or a filter that adds values.") + lint_ctx.error(f"Select parameter [{param_name}] options tag defines no options. Use 'from_dataset', 'from_data_table', or a filter that adds values.", line=options[0].sourceline) if from_file is not None: - lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'from_file' attribute.") + lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'from_file' attribute.", line=options[0].sourceline) if from_parameter is not None: - lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'from_parameter' attribute.") + lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'from_parameter' attribute.", line=options[0].sourceline) if from_dataset is not None and from_data_table is not None: - lint_ctx.error(f"Select parameter [{param_name}] options uses 'from_dataset' and 'from_data_table' attribute.") + lint_ctx.error(f"Select parameter [{param_name}] options uses 'from_dataset' and 'from_data_table' attribute.", line=options[0].sourceline) if options[0].get("meta_file_key", None) is not None and from_dataset is None: - lint_ctx.error(f"Select parameter [{param_name}] 'meta_file_key' is only compatible with 'from_dataset'.") + lint_ctx.error(f"Select parameter [{param_name}] 'meta_file_key' is only compatible with 'from_dataset'.", line=options[0].sourceline) if options[0].get("options_filter_attribute", None) is not None: - lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'options_filter_attribute' attribute.") + lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'options_filter_attribute' attribute.", line=options[0].sourceline) if options[0].get("transform_lines", None) is not None: - lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'transform_lines' attribute.") + lint_ctx.warn(f"Select parameter [{param_name}] options uses deprecated 'transform_lines' attribute.", line=options[0].sourceline) elif len(options) > 1: - lint_ctx.error(f"Select parameter [{param_name}] contains multiple options elements") + lint_ctx.error(f"Select parameter [{param_name}] contains multiple options elements", line=options[1].sourceline) # lint statically defined options if any('value' not in option.attrib for option in select_options): - lint_ctx.error(f"Select parameter [{param_name}] has option without value") + lint_ctx.error(f"Select parameter [{param_name}] has option without value", line=param.sourceline) if any(option.text is None for option in select_options): - lint_ctx.warn(f"Select parameter [{param_name}] has option without text") + lint_ctx.warn(f"Select parameter [{param_name}] has option without text", line=param.sourceline) select_options_texts = list() select_options_values = list() @@ -148,22 +153,22 @@ def lint_inputs(tool_xml, lint_ctx): select_options_texts.append((text, option.attrib.get("selected", "false"))) select_options_values.append((value, option.attrib.get("selected", "false"))) if len(set(select_options_texts)) != len(select_options_texts): - lint_ctx.error(f"Select parameter [{param_name}] has multiple options with the same text content") + lint_ctx.error(f"Select parameter [{param_name}] has multiple options with the same text content", line=param.sourceline) if len(set(select_options_values)) != len(select_options_values): - lint_ctx.error(f"Select parameter [{param_name}] has multiple options with the same value") + lint_ctx.error(f"Select parameter [{param_name}] has multiple options with the same value", line=param.sourceline) multiple = string_as_bool(param_attrib.get("multiple", "false")) optional = string_as_bool(param_attrib.get("optional", multiple)) if param_attrib.get("display") == "checkboxes": if not multiple: - lint_ctx.error(f'Select [{param_name}] `display="checkboxes"` is incompatible with `multiple="false"`, remove the `display` attribute') + lint_ctx.error(f'Select [{param_name}] `display="checkboxes"` is incompatible with `multiple="false"`, remove the `display` attribute', line=param.sourceline) if not optional: - lint_ctx.error(f'Select [{param_name}] `display="checkboxes"` is incompatible with `optional="false"`, remove the `display` attribute') + lint_ctx.error(f'Select [{param_name}] `display="checkboxes"` is incompatible with `optional="false"`, remove the `display` attribute', line=param.sourceline) if param_attrib.get("display") == "radio": if multiple: - lint_ctx.error(f'Select [{param_name}] display="radio" is incompatible with multiple="true"') + lint_ctx.error(f'Select [{param_name}] display="radio" is incompatible with multiple="true"', line=param.sourceline) if optional: - lint_ctx.error(f'Select [{param_name}] display="radio" is incompatible with optional="true"') + lint_ctx.error(f'Select [{param_name}] display="radio" is incompatible with optional="true"', line=param.sourceline) # TODO: Validate type, much more... # lint validators @@ -172,36 +177,36 @@ def lint_inputs(tool_xml, lint_ctx): vtype = validator.attrib['type'] if param_type in PARAMETER_VALIDATOR_TYPE_COMPATIBILITY: if vtype not in PARAMETER_VALIDATOR_TYPE_COMPATIBILITY[param_type]: - lint_ctx.error(f"Parameter [{param_name}]: validator with an incompatible type '{vtype}'") + lint_ctx.error(f"Parameter [{param_name}]: validator with an incompatible type '{vtype}'", line=validator.sourceline) for attrib in ATTRIB_VALIDATOR_COMPATIBILITY: if attrib in validator.attrib and vtype not in ATTRIB_VALIDATOR_COMPATIBILITY[attrib]: - lint_ctx.error(f"Parameter [{param_name}]: attribute '{attrib}' is incompatible with validator of type '{vtype}'") + lint_ctx.error(f"Parameter [{param_name}]: attribute '{attrib}' is incompatible with validator of type '{vtype}'", line=validator.sourceline) if vtype == "expression" and validator.text is None: lint_ctx.error(f"Parameter [{param_name}]: expression validator without content") if vtype not in ["expression", "regex"] and validator.text is not None: - lint_ctx.warn(f"Parameter [{param_name}]: '{vtype}' validators are not expected to contain text (found '{validator.text}')") + lint_ctx.warn(f"Parameter [{param_name}]: '{vtype}' validators are not expected to contain text (found '{validator.text}')", line=validator.sourceline) if vtype in ["in_range", "length", "dataset_metadata_in_range"] and ("min" not in validator.attrib and "max" not in validator.attrib): - lint_ctx.error(f"Parameter [{param_name}]: '{vtype}' validators need to define the 'min' or 'max' attribute(s)") + lint_ctx.error(f"Parameter [{param_name}]: '{vtype}' validators need to define the 'min' or 'max' attribute(s)", line=validator.sourceline) if vtype in ["metadata"] and ("check" not in validator.attrib and "skip" not in validator.attrib): - lint_ctx.error(f"Parameter [{param_name}]: '{vtype}' validators need to define the 'check' or 'skip' attribute(s) {validator.attrib}") + lint_ctx.error(f"Parameter [{param_name}]: '{vtype}' validators need to define the 'check' or 'skip' attribute(s) {validator.attrib}", line=validator.sourceline) if vtype in ["value_in_data_table", "value_not_in_data_table", "dataset_metadata_in_data_table", "dataset_metadata_not_in_data_table"] and "table_name" not in validator.attrib: - lint_ctx.error(f"Parameter [{param_name}]: '{vtype}' validators need to define the 'table_name' attribute") + lint_ctx.error(f"Parameter [{param_name}]: '{vtype}' validators need to define the 'table_name' attribute", line=validator.sourceline) conditional_selects = tool_xml.findall("./inputs//conditional") for conditional in conditional_selects: conditional_name = conditional.get('name') if not conditional_name: - lint_ctx.error("Conditional without a name") + lint_ctx.error("Conditional without a name", line=conditional.sourceline) if conditional.get("value_from"): # Probably only the upload tool use this, no children elements continue first_param = conditional.find("param") if first_param is None: - lint_ctx.error(f"Conditional [{conditional_name}] has no child ") + lint_ctx.error(f"Conditional [{conditional_name}] has no child ", line=conditional.sourceline) continue first_param_type = first_param.get('type') if first_param_type not in ['select', 'boolean']: - lint_ctx.warn(f'Conditional [{conditional_name}] first param should have type="select" /> or type="boolean"') + lint_ctx.warn(f'Conditional [{conditional_name}] first param should have type="select" /> or type="boolean"', line=first_param_type.sourceline) continue if first_param_type == 'select': @@ -214,37 +219,37 @@ def lint_inputs(tool_xml, lint_ctx): ] if string_as_bool(first_param.get('optional', False)): - lint_ctx.warn(f"Conditional [{conditional_name}] test parameter cannot be optional") + lint_ctx.warn(f"Conditional [{conditional_name}] test parameter cannot be optional", line=first_param_type.sourceline) whens = conditional.findall('./when') if any('value' not in when.attrib for when in whens): - lint_ctx.error(f"Conditional [{conditional_name}] when without value") + lint_ctx.error(f"Conditional [{conditional_name}] when without value", line=conditional.sourceline) when_ids = [w.get('value') for w in whens] for option_id in option_ids: if option_id not in when_ids: - lint_ctx.warn(f"Conditional [{conditional_name}] no block found for {first_param_type} option '{option_id}'") + lint_ctx.warn(f"Conditional [{conditional_name}] no block found for {first_param_type} option '{option_id}'", line=conditional.sourceline) for when_id in when_ids: if when_id not in option_ids: if first_param_type == 'select': - lint_ctx.warn(f"Conditional [{conditional_name}] no