diff --git a/changelogs/fragments/83530-validate-modules-casing.yml b/changelogs/fragments/83530-validate-modules-casing.yml new file mode 100644 index 00000000000..d00a344d2fe --- /dev/null +++ b/changelogs/fragments/83530-validate-modules-casing.yml @@ -0,0 +1,2 @@ +minor_changes: + - "validate-modules sanity test - reject option/aliases names that are identical up to casing but belong to different options (https://github.com/ansible/ansible/pull/83530)." diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/option_name_casing.py b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/option_name_casing.py new file mode 100644 index 00000000000..7ffd75bb7c4 --- /dev/null +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/option_name_casing.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import annotations + + +DOCUMENTATION = ''' +module: option_name_casing +short_description: Option names equal up to casing +description: Option names equal up to casing. +author: + - Ansible Core Team +options: + foo: + description: Foo + type: str + aliases: + - bar + - FOO # this one is ok + Foo: + description: Foo alias + type: str + Bar: + description: Bar alias + type: str + bam: + description: Bar alias 2 + aliases: + - baR + type: str +''' + +EXAMPLES = '''#''' +RETURN = '''''' + +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + module = AnsibleModule(argument_spec=dict( + foo=dict(type='str', aliases=['bar', 'FOO']), + Foo=dict(type='str'), + Bar=dict(type='str'), + bam=dict(type='str', aliases=['baR']) + )) + module.exit_json() diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt b/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt index 3ae113c5ef2..b01ec459d3c 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt +++ b/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt @@ -12,6 +12,8 @@ plugins/modules/invalid_yaml_syntax.py:0:0: missing-documentation: No DOCUMENTAT plugins/modules/invalid_yaml_syntax.py:7:15: documentation-syntax-error: DOCUMENTATION is not valid YAML plugins/modules/invalid_yaml_syntax.py:11:15: invalid-examples: EXAMPLES is not valid YAML plugins/modules/invalid_yaml_syntax.py:15:15: return-syntax-error: RETURN is not valid YAML +plugins/modules/option_name_casing.py:0:0: option-equal-up-to-casing: Multiple options/aliases are equal up to casing: option 'Bar', alias 'baR' of option 'bam', alias 'bar' of option 'foo' +plugins/modules/option_name_casing.py:0:0: option-equal-up-to-casing: Multiple options/aliases are equal up to casing: option 'Foo', option 'foo' plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.0: While parsing "V(C\(" at index 1: Unnecessarily escaped "(" @ data['options']['a11']['suboptions']['b1']['description'][0]. Got 'V(C\\(foo\\)).' plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.2: While parsing "P(foo.bar#baz)" at index 1: Plugin name "foo.bar" is not a FQCN @ data['options']['a11']['suboptions']['b1']['description'][2]. Got 'P(foo.bar#baz).' plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.3: While parsing "P(foo.bar.baz)" at index 1: Parameter "foo.bar.baz" is not of the form FQCN#type @ data['options']['a11']['suboptions']['b1']['description'][3]. Got 'P(foo.bar.baz).' diff --git a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py index ddfb8ca72d2..990076e5bc6 100644 --- a/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +++ b/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py @@ -838,6 +838,46 @@ class ModuleValidator(Validator): msg='%s: %s' % (combined_path, error_message) ) + def _validate_option_docs(self, options, context=None): + if not isinstance(options, dict): + return + if context is None: + context = [] + + normalized_option_alias_names = dict() + + def add_option_alias_name(name, option_name): + normalized_name = str(name).lower() + normalized_option_alias_names.setdefault(normalized_name, {}).setdefault(option_name, set()).add(name) + + for option, data in options.items(): + if 'suboptions' in data: + self._validate_option_docs(data.get('suboptions'), context + [option]) + add_option_alias_name(option, option) + if 'aliases' in data and isinstance(data['aliases'], list): + for alias in data['aliases']: + add_option_alias_name(alias, option) + + for normalized_name, options in normalized_option_alias_names.items(): + if len(options) < 2: + continue + + what = [] + for option_name, names in sorted(options.items()): + if option_name in names: + what.append("option '%s'" % option_name) + else: + what.append("alias '%s' of option '%s'" % (sorted(names)[0], option_name)) + msg = "Multiple options/aliases" + if context: + msg += " found in %s" % " -> ".join(context) + msg += " are equal up to casing: %s" % ", ".join(what) + self.reporter.error( + path=self.object_path, + code='option-equal-up-to-casing', + msg=msg, + ) + def _validate_docs(self): doc = None # We have three ways of marking deprecated/removed files. Have to check each one @@ -1015,6 +1055,9 @@ class ModuleValidator(Validator): 'invalid-documentation', ) + if doc: + self._validate_option_docs(doc.get('options')) + self._validate_all_semantic_markup(doc, returns) if not self.collection: