330 lines
9.5 KiB
Python
330 lines
9.5 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2015 Google LLC. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
"""A command that validates gcloud flags according to Cloud SDK CLI Style."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import io
|
|
import os
|
|
|
|
from googlecloudsdk.calliope import arg_parsers
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import usage_text
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core.util import files
|
|
|
|
import six
|
|
|
|
|
|
class UnknownCheckException(Exception):
|
|
"""An exception when unknown lint check is requested."""
|
|
|
|
|
|
class LintException(exceptions.Error):
|
|
"""One or more lint errors found."""
|
|
|
|
|
|
class LintError(object):
|
|
"""Validation failure.
|
|
|
|
Attributes:
|
|
name: str, The name of the validation that produced this failure.
|
|
command: calliope.backend.CommandCommon, The offending command.
|
|
msg: str, A message indicating what the problem was.
|
|
"""
|
|
|
|
def __init__(self, name, command, error_message):
|
|
self.name = name
|
|
self.command = command
|
|
self.msg = '[{cmd}]: {msg}'.format(
|
|
cmd='.'.join(command.GetPath()), msg=error_message)
|
|
|
|
|
|
class Checker(object):
|
|
"""The abstract base class for all the checks.
|
|
|
|
Attributes:
|
|
name: A string, the name of this Checker.
|
|
description: string, command line description of this check.
|
|
"""
|
|
|
|
def ForEveryGroup(self, group):
|
|
pass
|
|
|
|
def ForEveryCommand(self, command):
|
|
pass
|
|
|
|
def End(self):
|
|
return []
|
|
|
|
|
|
class NameChecker(Checker):
|
|
"""Checks if group,command and flags names have underscores or mixed case."""
|
|
|
|
name = 'NameCheck'
|
|
description = 'Verifies all existing flags not to have underscores.'
|
|
|
|
def __init__(self):
|
|
super(NameChecker, self).__init__()
|
|
self._issues = []
|
|
|
|
def _ForEvery(self, cmd_or_group):
|
|
"""Run name check for given command or group."""
|
|
|
|
if '_' in cmd_or_group.cli_name:
|
|
self._issues.append(LintError(
|
|
name=NameChecker.name,
|
|
command=cmd_or_group,
|
|
error_message='command name [{0}] has underscores'.format(
|
|
cmd_or_group.cli_name)))
|
|
|
|
if not (cmd_or_group.cli_name.islower() or cmd_or_group.cli_name.isupper()):
|
|
self._issues.append(LintError(
|
|
name=NameChecker.name,
|
|
command=cmd_or_group,
|
|
error_message='command name [{0}] mixed case'.format(
|
|
cmd_or_group.cli_name)))
|
|
|
|
for flag in cmd_or_group.GetSpecificFlags():
|
|
if not any(f.startswith('--') for f in flag.option_strings):
|
|
if len(flag.option_strings) != 1 or flag.option_strings[0] != '-h':
|
|
self._issues.append(LintError(
|
|
name=NameChecker.name,
|
|
command=cmd_or_group,
|
|
error_message='flag [{0}] has no long form'.format(
|
|
','.join(flag.option_strings))))
|
|
for flag_option_string in flag.option_strings:
|
|
msg = None
|
|
if '_' in flag_option_string:
|
|
msg = 'flag [%s] has underscores' % flag_option_string
|
|
if (flag_option_string.startswith('--')
|
|
and not flag_option_string.islower()):
|
|
msg = 'long flag [%s] has upper case characters' % flag_option_string
|
|
if msg:
|
|
self._issues.append(LintError(
|
|
name=NameChecker.name, command=cmd_or_group, error_message=msg))
|
|
|
|
def ForEveryGroup(self, group):
|
|
self._ForEvery(group)
|
|
|
|
def ForEveryCommand(self, command):
|
|
self._ForEvery(command)
|
|
|
|
def End(self):
|
|
return self._issues
|
|
|
|
|
|
class BadListsChecker(Checker):
|
|
"""Checks command flags that take lists."""
|
|
|
|
name = 'BadLists'
|
|
description = 'Verifies all flags implement lists properly.'
|
|
|
|
def __init__(self):
|
|
super(BadListsChecker, self).__init__()
|
|
self._issues = []
|
|
|
|
def _ForEvery(self, cmd_or_group):
|
|
for flag in cmd_or_group.GetSpecificFlags():
|
|
if flag.nargs not in [None, 0, 1]:
|
|
self._issues.append(LintError(
|
|
name=BadListsChecker.name,
|
|
command=cmd_or_group,
|
|
error_message=(
|
|
'flag [{flg}] has nargs={nargs}'.format(
|
|
flg=flag.option_strings[0],
|
|
nargs="'{}'".format(six.text_type(flag.nargs))))))
|
|
if isinstance(flag.type, arg_parsers.ArgObject):
|
|
# No metavar requirements for ArgObject.
|
|
return
|
|
if isinstance(flag.type, arg_parsers.ArgDict):
|
|
if not (flag.metavar or flag.type.spec):
|
|
self._issues.append(
|
|
LintError(
|
|
name=BadListsChecker.name,
|
|
command=cmd_or_group,
|
|
error_message=(
|
|
('dict flag [{flg}] has no metavar and type.spec'
|
|
' (at least one needed)'
|
|
).format(flg=flag.option_strings[0]))))
|
|
elif isinstance(flag.type, arg_parsers.ArgList):
|
|
if not flag.metavar:
|
|
self._issues.append(LintError(
|
|
name=BadListsChecker.name,
|
|
command=cmd_or_group,
|
|
error_message=(
|
|
'list flag [{flg}] has no metavar'.format(
|
|
flg=flag.option_strings[0]))))
|
|
|
|
def ForEveryGroup(self, group):
|
|
self._ForEvery(group)
|
|
|
|
def ForEveryCommand(self, command):
|
|
self._ForEvery(command)
|
|
|
|
def End(self):
|
|
return self._issues
|
|
|
|
|
|
def _GetAllowlistedCommandVocabulary():
|
|
"""Returns allowlisted set of gcloud commands."""
|
|
|
|
vocabulary_file = os.path.join(os.path.dirname(__file__),
|
|
'gcloud_command_vocabulary.txt')
|
|
return set(
|
|
line for line in files.ReadFileContents(vocabulary_file).split('\n')
|
|
if not line.startswith('#'))
|
|
|
|
|
|
class VocabularyChecker(Checker):
|
|
"""Checks that command is the list of allowlisted names."""
|
|
|
|
name = 'AllowlistedNameCheck'
|
|
description = 'Verifies that every command is allowlisted.'
|
|
|
|
def __init__(self):
|
|
super(VocabularyChecker, self).__init__()
|
|
self._allowlist = _GetAllowlistedCommandVocabulary()
|
|
self._issues = []
|
|
|
|
def ForEveryGroup(self, group):
|
|
pass
|
|
|
|
def ForEveryCommand(self, command):
|
|
if command.cli_name not in self._allowlist:
|
|
self._issues.append(LintError(
|
|
name=self.name,
|
|
command=command,
|
|
error_message='command name [{0}] is not allowlisted'.format(
|
|
command.cli_name)))
|
|
|
|
def End(self):
|
|
return self._issues
|
|
|
|
|
|
def _WalkGroupTree(group):
|
|
"""Visits each group in the CLI group tree.
|
|
|
|
Args:
|
|
group: backend.CommandGroup, root CLI subgroup node.
|
|
Yields:
|
|
group instance.
|
|
"""
|
|
|
|
yield group
|
|
|
|
for sub_group in six.itervalues(group.groups):
|
|
for value in _WalkGroupTree(sub_group):
|
|
yield value
|
|
|
|
|
|
class Linter(object):
|
|
"""Lints gcloud commands."""
|
|
|
|
def __init__(self):
|
|
self._checks = []
|
|
|
|
def AddCheck(self, check):
|
|
self._checks.append(check())
|
|
|
|
def Run(self, group_root):
|
|
"""Runs registered checks on all groups and commands."""
|
|
for group in _WalkGroupTree(group_root):
|
|
for check in self._checks:
|
|
check.ForEveryGroup(group)
|
|
for command in six.itervalues(group.commands):
|
|
for check in self._checks:
|
|
check.ForEveryCommand(command)
|
|
|
|
return [issue for check in self._checks for issue in check.End()]
|
|
|
|
|
|
# List of registered checks, all are run by default.
|
|
_DEFAULT_LINT_CHECKS = [
|
|
NameChecker,
|
|
]
|
|
|
|
_LINT_CHECKS = [
|
|
BadListsChecker,
|
|
VocabularyChecker,
|
|
]
|
|
|
|
|
|
def _FormatCheckList(check_list):
|
|
buf = io.StringIO()
|
|
for check in check_list:
|
|
usage_text.WrapWithPrefix(
|
|
check.name, check.description, 20, 78, ' ', writer=buf)
|
|
return buf.getvalue()
|
|
|
|
|
|
class Lint(base.Command):
|
|
"""Validate gcloud flags according to Cloud SDK CLI Style."""
|
|
|
|
@staticmethod
|
|
def Args(parser):
|
|
parser.add_argument(
|
|
'checks',
|
|
metavar='CHECKS',
|
|
nargs='*',
|
|
default=[],
|
|
help="""\
|
|
A list of checks to apply to gcloud groups and commands.
|
|
If omitted will run all available checks.
|
|
Available Checks:
|
|
""" + _FormatCheckList(_LINT_CHECKS))
|
|
|
|
def Run(self, args):
|
|
# pylint: disable=protected-access
|
|
group = self._cli_power_users_only._TopElement()
|
|
group.LoadAllSubElements(recursive=True)
|
|
return Lint._SetupAndRun(group, args.checks)
|
|
|
|
@staticmethod
|
|
def _SetupAndRun(group, check_list):
|
|
"""Builds up linter and executes it for given set of checks."""
|
|
|
|
linter = Linter()
|
|
unknown_checks = []
|
|
if not check_list:
|
|
for check in _DEFAULT_LINT_CHECKS:
|
|
linter.AddCheck(check)
|
|
else:
|
|
available_checkers = dict(
|
|
(checker.name, checker)
|
|
for checker in _DEFAULT_LINT_CHECKS + _LINT_CHECKS)
|
|
for check in check_list:
|
|
if check in available_checkers:
|
|
linter.AddCheck(available_checkers[check])
|
|
else:
|
|
unknown_checks.append(check)
|
|
|
|
if unknown_checks:
|
|
raise UnknownCheckException(
|
|
'Unknown lint checks: %s' % ','.join(unknown_checks))
|
|
|
|
return linter.Run(group)
|
|
|
|
def Display(self, args, result):
|
|
writer = log.out
|
|
for issue in result:
|
|
writer.Print(issue.msg)
|
|
if result:
|
|
raise LintException('there were some lint errors.')
|