0ff37ff859
In some merge issues files or modules can be removed, but still registered in global env. In that cases custom guidelines raises an error with the message "No such file or directory". Add exception handling to avoid error raising. Instead of error, call warning and skip such class. Change-Id: I71af2fffc2bdec414cb68c40eaf3268fe81134de Closes-bug: #1613669
286 lines
12 KiB
Python
286 lines
12 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
|
|
from oslo_log import log
|
|
import six
|
|
|
|
from heat.common.i18n import _
|
|
from heat.common.i18n import _LW
|
|
from heat.engine import constraints
|
|
from heat.engine import resources
|
|
from heat.engine import support
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
class HeatCustomGuidelines(object):
|
|
|
|
_RULES = ['resource_descriptions', 'trailing_spaces']
|
|
|
|
def __init__(self, exclude):
|
|
self.error_count = 0
|
|
self.resources_classes = []
|
|
global_env = resources.global_env()
|
|
for resource_type in global_env.get_types():
|
|
cls = global_env.get_class(resource_type)
|
|
module = cls.__module__
|
|
# Skip resources, which defined as template resource in environment
|
|
if module == 'heat.engine.resources.template_resource':
|
|
continue
|
|
# Skip discovered plugin resources
|
|
if module == 'heat.engine.plugins':
|
|
continue
|
|
path = module.replace('.', '/')
|
|
if any(path.startswith(excl_path) for excl_path in exclude):
|
|
continue
|
|
self.resources_classes.append(cls)
|
|
|
|
def run_check(self):
|
|
print(_('Heat custom guidelines check started.'))
|
|
for rule in self._RULES:
|
|
getattr(self, 'check_%s' % rule)()
|
|
if self.error_count > 0:
|
|
print(_('Heat custom guidelines check failed - '
|
|
'found %s errors.') % self.error_count)
|
|
sys.exit(1)
|
|
else:
|
|
print(_('Heat custom guidelines check succeeded.'))
|
|
|
|
def check_resource_descriptions(self):
|
|
for cls in self.resources_classes:
|
|
# check resource's description
|
|
self._check_resource_description(cls)
|
|
# check properties' descriptions
|
|
self._check_resource_schemas(cls, cls.properties_schema,
|
|
'property')
|
|
# check attributes' descriptions
|
|
self._check_resource_schemas(cls, cls.attributes_schema,
|
|
'attribute')
|
|
# check methods descriptions
|
|
self._check_resource_methods(cls)
|
|
|
|
def _check_resource_description(self, resource):
|
|
description = resource.__doc__
|
|
if resource.support_status.status not in (support.SUPPORTED,
|
|
support.UNSUPPORTED):
|
|
return
|
|
kwargs = {'path': resource.__module__, 'details': resource.__name__}
|
|
if not description:
|
|
kwargs.update({'message': _("Resource description missing, "
|
|
"should add resource description "
|
|
"about resource's purpose")})
|
|
self.print_guideline_error(**kwargs)
|
|
return
|
|
|
|
doclines = [key.strip() for key in description.split('\n')]
|
|
if len(doclines) == 1 or (len(doclines) == 2 and doclines[-1] == ''):
|
|
kwargs.update({'message': _("Resource description missing, "
|
|
"should add resource description "
|
|
"about resource's purpose")})
|
|
self.print_guideline_error(**kwargs)
|
|
return
|
|
|
|
self._check_description_summary(doclines[0], kwargs, 'resource')
|
|
self._check_description_details(doclines, kwargs, 'resource')
|
|
|
|
def _check_resource_schemas(self, resource, schema, schema_name,
|
|
error_path=None):
|
|
for key, value in six.iteritems(schema):
|
|
if error_path is None:
|
|
error_path = [resource.__name__, key]
|
|
else:
|
|
error_path.append(key)
|
|
# need to check sub-schema of current schema, if exists
|
|
if (hasattr(value, 'schema') and
|
|
getattr(value, 'schema') is not None):
|
|
self._check_resource_schemas(resource, value.schema,
|
|
schema_name, error_path)
|
|
description = value.description
|
|
kwargs = {'path': resource.__module__, 'details': error_path}
|
|
if description is None:
|
|
if (value.support_status.status == support.SUPPORTED and
|
|
not isinstance(value.schema,
|
|
constraints.AnyIndexDict) and
|
|
not isinstance(schema, constraints.AnyIndexDict)):
|
|
kwargs.update({'message': _("%s description "
|
|
"missing, need to add "
|
|
"description about property's "
|
|
"purpose") % schema_name})
|
|
self.print_guideline_error(**kwargs)
|
|
error_path.pop()
|
|
continue
|
|
self._check_description_summary(description, kwargs, schema_name)
|
|
error_path.pop()
|
|
|
|
def _check_resource_methods(self, resource):
|
|
for method in six.itervalues(resource.__dict__):
|
|
# need to skip non-functions attributes
|
|
if not callable(method):
|
|
continue
|
|
description = method.__doc__
|
|
if not description:
|
|
continue
|
|
if method.__name__.startswith('__'):
|
|
continue
|
|
doclines = [key.strip() for key in description.split('\n')]
|
|
kwargs = {'path': resource.__module__,
|
|
'details': [resource.__name__, method.__name__]}
|
|
|
|
self._check_description_summary(doclines[0], kwargs, 'method')
|
|
|
|
if len(doclines) == 2:
|
|
kwargs.update({'message': _('Method description summary '
|
|
'should be in one line')})
|
|
self.print_guideline_error(**kwargs)
|
|
continue
|
|
|
|
if len(doclines) > 1:
|
|
self._check_description_details(doclines, kwargs, 'method')
|
|
|
|
def check_trailing_spaces(self):
|
|
for cls in self.resources_classes:
|
|
try:
|
|
cls_file = open(cls.__module__.replace('.', '/') + '.py')
|
|
except IOError as ex:
|
|
LOG.warning(_LW('Cannot perform trailing spaces check on '
|
|
'resource module: %s') % six.text_type(ex))
|
|
continue
|
|
lines = [line.strip() for line in cls_file.readlines()]
|
|
idx = 0
|
|
kwargs = {'path': cls.__module__}
|
|
while idx < len(lines):
|
|
if ('properties_schema' in lines[idx] or
|
|
'attributes_schema' in lines[idx]):
|
|
level = len(re.findall('(\{|\()', lines[idx]))
|
|
level -= len(re.findall('(\}|\))', lines[idx]))
|
|
idx += 1
|
|
while level != 0:
|
|
level += len(re.findall('(\{|\()', lines[idx]))
|
|
level -= len(re.findall('(\}|\))', lines[idx]))
|
|
if re.search("^((\'|\") )", lines[idx]):
|
|
kwargs.update(
|
|
{'details': 'line %s' % idx,
|
|
'message': _('Trailing whitespace should '
|
|
'be on previous line'),
|
|
'snippet': lines[idx]})
|
|
self.print_guideline_error(**kwargs)
|
|
elif (re.search("(\S(\'|\"))$", lines[idx - 1]) and
|
|
re.search("^((\'|\")\S)", lines[idx])):
|
|
kwargs.update(
|
|
{'details': 'line %s' % (idx - 1),
|
|
'message': _('Omitted whitespace at the '
|
|
'end of the line'),
|
|
'snippet': lines[idx - 1]})
|
|
self.print_guideline_error(**kwargs)
|
|
idx += 1
|
|
idx += 1
|
|
|
|
def _check_description_summary(self, description, error_kwargs,
|
|
error_key):
|
|
if re.search("^[a-z]", description):
|
|
error_kwargs.update(
|
|
{'message': _('%s description summary should start '
|
|
'with uppercase letter') % error_key.title(),
|
|
'snippet': description})
|
|
self.print_guideline_error(**error_kwargs)
|
|
if not description.endswith('.'):
|
|
error_kwargs.update(
|
|
{'message': _('%s description summary omitted '
|
|
'terminator at the end') % error_key.title(),
|
|
'snippet': description})
|
|
self.print_guideline_error(**error_kwargs)
|
|
if re.search("\s{2,}", description):
|
|
error_kwargs.update(
|
|
{'message': _('%s description contains double or more '
|
|
'whitespaces') % error_key.title(),
|
|
'snippet': description})
|
|
self.print_guideline_error(**error_kwargs)
|
|
|
|
def _check_description_details(self, doclines, error_kwargs,
|
|
error_key):
|
|
if re.search("\S", doclines[1]):
|
|
error_kwargs.update(
|
|
{'message': _('%s description summary and '
|
|
'main resource description should be '
|
|
'separated by blank line') % error_key.title(),
|
|
'snippet': doclines[0]})
|
|
self.print_guideline_error(**error_kwargs)
|
|
|
|
if re.search("^[a-z]", doclines[2]):
|
|
error_kwargs.update(
|
|
{'message': _('%s description should start '
|
|
'with with uppercase '
|
|
'letter') % error_key.title(),
|
|
'snippet': doclines[2]})
|
|
self.print_guideline_error(**error_kwargs)
|
|
|
|
if doclines[-1] != '':
|
|
error_kwargs.update(
|
|
{'message': _('%s description multistring '
|
|
'should have singly closing quotes at '
|
|
'the next line') % error_key.title(),
|
|
'snippet': doclines[-1]})
|
|
self.print_guideline_error(**error_kwargs)
|
|
|
|
params = False
|
|
for line in doclines[1:]:
|
|
if re.search("\s{2,}", line):
|
|
error_kwargs.update(
|
|
{'message': _('%s description '
|
|
'contains double or more '
|
|
'whitespaces') % error_key.title(),
|
|
'snippet': line})
|
|
self.print_guideline_error(**error_kwargs)
|
|
if re.search("^(:param|:type|:returns|:rtype|:raises)",
|
|
line):
|
|
params = True
|
|
if not params and not doclines[-2].endswith('.'):
|
|
error_kwargs.update(
|
|
{'message': _('%s description omitted '
|
|
'terminator at the end') % error_key.title(),
|
|
'snippet': doclines[-2]})
|
|
self.print_guideline_error(**error_kwargs)
|
|
|
|
def print_guideline_error(self, path, details, message, snippet=None):
|
|
if isinstance(details, list):
|
|
details = '.'.join(details)
|
|
msg = _('ERROR (in %(path)s: %(details)s): %(message)s') % {
|
|
'message': message,
|
|
'path': path.replace('.', '/'),
|
|
'details': details
|
|
}
|
|
if snippet is not None:
|
|
msg = _('%(msg)s\n (Error snippet): %(snippet)s') % {
|
|
'msg': msg,
|
|
'snippet': '%s...' % snippet[:79]
|
|
}
|
|
print(msg)
|
|
self.error_count += 1
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--exclude', '-e', metavar='<FOLDER>',
|
|
nargs='+',
|
|
help=_('Exclude specified paths from checking.'))
|
|
return parser.parse_args()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
args = parse_args()
|
|
guidelines = HeatCustomGuidelines(args.exclude or [])
|
|
guidelines.run_check()
|