Validate some markup in documentation in validate-modules (#76262)

* Validate some markup in documentation.

* Add changelog fragment.

* Use urlparse instead of URL regex.

* Document new error code.

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
pull/76284/head
Felix Fontein 3 years ago committed by GitHub
parent 9985b8a975
commit fe77bc9e3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
minor_changes:
- "validate-modules - do some basic validation on the ``M(...)``, ``U(...)``, ``L(..., ...)`` and ``R(..., ...)`` documentation markups (https://github.com/ansible/ansible/pull/76262)."

@ -90,6 +90,7 @@ Codes
invalid-argument-spec Documentation Error Argument in argument_spec must be a dictionary/hash when used invalid-argument-spec Documentation Error Argument in argument_spec must be a dictionary/hash when used
invalid-argument-spec-options Documentation Error Suboptions in argument_spec are invalid invalid-argument-spec-options Documentation Error Suboptions in argument_spec are invalid
invalid-documentation Documentation Error ``DOCUMENTATION`` is not valid YAML invalid-documentation Documentation Error ``DOCUMENTATION`` is not valid YAML
invalid-documentation-markup Documentation Error ``DOCUMENTATION`` or ``RETURN`` contains invalid markup
invalid-documentation-options Documentation Error ``DOCUMENTATION.options`` must be a dictionary/hash when used invalid-documentation-options Documentation Error ``DOCUMENTATION.options`` must be a dictionary/hash when used
invalid-examples Documentation Error ``EXAMPLES`` is not valid YAML invalid-examples Documentation Error ``EXAMPLES`` is not valid YAML
invalid-extension Naming Error Official Ansible modules must have a ``.py`` extension for python modules or a ``.ps1`` for powershell modules invalid-extension Naming Error Official Ansible modules must have a ``.py`` extension for python modules or a ``.ps1`` for powershell modules

@ -10,6 +10,7 @@ import re
from ansible.module_utils.compat.version import StrictVersion from ansible.module_utils.compat.version import StrictVersion
from functools import partial from functools import partial
from urllib.parse import urlparse
from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
@ -46,7 +47,8 @@ def isodate(v, error_code=None):
return v return v
COLLECTION_NAME_RE = re.compile(r'^([^.]+(\.[^.]+)+)$') COLLECTION_NAME_RE = re.compile(r'^\w+(?:\.\w+)+$')
FULLY_QUALIFIED_COLLECTION_RESOURCE_RE = re.compile(r'^\w+(?:\.\w+){2,}$')
def collection_name(v, error_code=None): def collection_name(v, error_code=None):
@ -77,6 +79,70 @@ def date(error_code=None):
return Any(isodate, error_code=error_code) return Any(isodate, error_code=error_code)
_MODULE = re.compile(r"\bM\(([^)]+)\)")
_LINK = re.compile(r"\bL\(([^)]+)\)")
_URL = re.compile(r"\bU\(([^)]+)\)")
_REF = re.compile(r"\bR\(([^)]+)\)")
def _check_module_link(directive, content):
if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(content):
raise _add_ansible_error_code(
Invalid('Directive "%s" must contain a FQCN' % directive), 'invalid-documentation-markup')
def _check_link(directive, content):
if ',' not in content:
raise _add_ansible_error_code(
Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
idx = content.rindex(',')
title = content[:idx]
url = content[idx + 1:].lstrip(' ')
_check_url(directive, url)
def _check_url(directive, content):
try:
parsed_url = urlparse(content)
if parsed_url.scheme not in ('', 'http', 'https'):
raise ValueError('Schema must be HTTP, HTTPS, or not specified')
except ValueError as exc:
raise _add_ansible_error_code(
Invalid('Directive "%s" must contain an URL' % directive), 'invalid-documentation-markup')
def _check_ref(directive, content):
if ',' not in content:
raise _add_ansible_error_code(
Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
def doc_string(v):
"""Match a documentation string."""
if not isinstance(v, string_types):
raise _add_ansible_error_code(
Invalid('Must be a string'), 'invalid-documentation')
for m in _MODULE.finditer(v):
_check_module_link(m.group(0), m.group(1))
for m in _LINK.finditer(v):
_check_link(m.group(0), m.group(1))
for m in _URL.finditer(v):
_check_url(m.group(0), m.group(1))
for m in _REF.finditer(v):
_check_ref(m.group(0), m.group(1))
return v
def doc_string_or_strings(v):
"""Match a documentation string, or list of strings."""
if isinstance(v, string_types):
return doc_string(v)
if isinstance(v, (list, tuple)):
return [doc_string(vv) for vv in v]
raise _add_ansible_error_code(
Invalid('Must be a string or list of strings'), 'invalid-documentation')
def is_callable(v): def is_callable(v):
if not callable(v): if not callable(v):
raise ValueInvalid('not a valid value') raise ValueInvalid('not a valid value')
@ -103,16 +169,16 @@ seealso_schema = Schema(
Any( Any(
{ {
Required('module'): Any(*string_types), Required('module'): Any(*string_types),
'description': Any(*string_types), 'description': doc_string,
}, },
{ {
Required('ref'): Any(*string_types), Required('ref'): Any(*string_types),
Required('description'): Any(*string_types), Required('description'): doc_string,
}, },
{ {
Required('name'): Any(*string_types), Required('name'): Any(*string_types),
Required('link'): Any(*string_types), Required('link'): Any(*string_types),
Required('description'): Any(*string_types), Required('description'): doc_string,
}, },
), ),
] ]
@ -322,7 +388,7 @@ def version_added(v, error_code='version-added-invalid', accept_historical=False
def list_dict_option_schema(for_collection): def list_dict_option_schema(for_collection):
suboption_schema = Schema( suboption_schema = Schema(
{ {
Required('description'): Any(list_string_types, *string_types), Required('description'): doc_string_or_strings,
'required': bool, 'required': bool,
'choices': list, 'choices': list,
'aliases': Any(list_string_types), 'aliases': Any(list_string_types),
@ -345,7 +411,7 @@ def list_dict_option_schema(for_collection):
option_schema = Schema( option_schema = Schema(
{ {
Required('description'): Any(list_string_types, *string_types), Required('description'): doc_string_or_strings,
'required': bool, 'required': bool,
'choices': list, 'choices': list,
'aliases': Any(list_string_types), 'aliases': Any(list_string_types),
@ -390,8 +456,8 @@ def return_schema(for_collection):
All( All(
Schema( Schema(
{ {
Required('description'): Any(list_string_types, *string_types), Required('description'): doc_string_or_strings,
'returned': Any(*string_types), # only returned on top level 'returned': doc_string, # only returned on top level
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'), Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': version(for_collection), 'version_added': version(for_collection),
'version_added_collection': collection_name, 'version_added_collection': collection_name,
@ -418,8 +484,8 @@ def return_schema(for_collection):
Schema( Schema(
{ {
any_string_types: { any_string_types: {
Required('description'): Any(list_string_types, *string_types), Required('description'): doc_string_or_strings,
Required('returned'): Any(*string_types), Required('returned'): doc_string,
Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'), Required('type'): Any('bool', 'complex', 'dict', 'float', 'int', 'list', 'str'),
'version_added': version(for_collection), 'version_added': version(for_collection),
'version_added_collection': collection_name, 'version_added_collection': collection_name,
@ -441,8 +507,8 @@ def return_schema(for_collection):
def deprecation_schema(for_collection): def deprecation_schema(for_collection):
main_fields = { main_fields = {
Required('why'): Any(*string_types), Required('why'): doc_string,
Required('alternative'): Any(*string_types), Required('alternative'): doc_string,
Required('removed_from_collection'): collection_name, Required('removed_from_collection'): collection_name,
'removed': Any(True), 'removed': Any(True),
} }
@ -502,13 +568,13 @@ def doc_schema(module_name, for_collection=False, deprecated_module=False):
deprecated_module = True deprecated_module = True
doc_schema_dict = { doc_schema_dict = {
Required('module'): module_name, Required('module'): module_name,
Required('short_description'): Any(*string_types), Required('short_description'): doc_string,
Required('description'): Any(list_string_types, *string_types), Required('description'): doc_string_or_strings,
Required('author'): All(Any(None, list_string_types, *string_types), author), Required('author'): All(Any(None, list_string_types, *string_types), author),
'notes': Any(None, list_string_types), 'notes': Any(None, [doc_string]),
'seealso': Any(None, seealso_schema), 'seealso': Any(None, seealso_schema),
'requirements': list_string_types, 'requirements': [doc_string],
'todo': Any(None, list_string_types, *string_types), 'todo': Any(None, doc_string_or_strings),
'options': Any(None, *list_dict_option_schema(for_collection)), 'options': Any(None, *list_dict_option_schema(for_collection)),
'extends_documentation_fragment': Any(list_string_types, *string_types), 'extends_documentation_fragment': Any(list_string_types, *string_types),
'version_added_collection': collection_name, 'version_added_collection': collection_name,
@ -529,8 +595,8 @@ def doc_schema(module_name, for_collection=False, deprecated_module=False):
def add_default_attributes(more=None): def add_default_attributes(more=None):
schema = { schema = {
'description': Any(list_string_types, *string_types), 'description': doc_string_or_strings,
'details': Any(list_string_types, *string_types), 'details': doc_string_or_strings,
'support': any_string_types, 'support': any_string_types,
'version_added_collection': any_string_types, 'version_added_collection': any_string_types,
'version_added': any_string_types, 'version_added': any_string_types,
@ -543,9 +609,9 @@ def doc_schema(module_name, for_collection=False, deprecated_module=False):
All( All(
Schema({ Schema({
any_string_types: { any_string_types: {
Required('description'): Any(list_string_types, *string_types), Required('description'): doc_string_or_strings,
Required('support'): Any('full', 'partial', 'none', 'N/A'), Required('support'): Any('full', 'partial', 'none', 'N/A'),
'details': Any(list_string_types, *string_types), 'details': doc_string_or_strings,
'version_added_collection': collection_name, 'version_added_collection': collection_name,
'version_added': version(for_collection=for_collection), 'version_added': version(for_collection=for_collection),
}, },

Loading…
Cancel
Save