1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
from ..rule_decorators import help_file_entry_rule
from ..linter import RuleError
from ..util import LinterError
import shlex
from unittest import mock
import re
# 'az' space then repeating runs of quoted tokens or non quoted characters
_az_pattern = 'az\s*' + '(([^\"\'])*|' + '((\"[^\"]*\"\s*)|(\'[^\']*\'\s*))' + ')'
# match the two types of command substitutions
_CMD_SUB_1 = re.compile("\$\(\s*" + "(" + _az_pattern + ")" + "\)")
_CMD_SUB_2 = re.compile("`\s*" + "(" + _az_pattern + ")" + "`")
from knack.log import get_logger
logger = get_logger(__name__)
@help_file_entry_rule
def unrecognized_help_entry_rule(linter, help_entry):
if help_entry not in linter.commands and help_entry not in linter.command_groups:
raise RuleError('Not a recognized command or command-group')
@help_file_entry_rule
def faulty_help_type_rule(linter, help_entry):
if linter.get_help_entry_type(help_entry) != 'group' and help_entry in linter.command_groups:
raise RuleError('Command-group should be of help-type `group`')
elif linter.get_help_entry_type(help_entry) != 'command' and help_entry in linter.commands:
raise RuleError('Command should be of help-type `command`')
@help_file_entry_rule
def unrecognized_help_parameter_rule(linter, help_entry):
if help_entry not in linter.commands:
return
param_help_names = linter.get_help_entry_parameter_names(help_entry)
violations = []
for param_help_name in param_help_names:
if not linter.is_valid_parameter_help_name(help_entry, param_help_name):
violations.append(param_help_name)
if violations:
raise RuleError('The following parameter help names are invalid: {}'.format(' | '.join(violations)))
@help_file_entry_rule
def faulty_help_example_rule(linter, help_entry):
violations = []
for index, example in enumerate(linter.get_help_entry_examples(help_entry)):
if 'az '+ help_entry not in example.get('text', ''):
violations.append(str(index))
if violations:
raise RuleError('The following example entry indices do not include the command: {}'.format(
' | '.join(violations)))
@help_file_entry_rule
def faulty_help_example_parameters_rule(linter, help_entry):
parser = linter.command_parser
violations = []
for example in linter.get_help_entry_examples(help_entry):
supported_profiles = example.get('supported-profiles')
if supported_profiles and 'latest' not in supported_profiles:
logger.warning("\n\tSKIPPING example: {}\n\tas 'latest' is not in its supported profiles."
"\n\t\tsupported-profiles: {}.".format(example['text'], example['supported-profiles']))
continue
unsupported_profiles = example.get('unsupported-profiles')
if unsupported_profiles and 'latest' in unsupported_profiles:
logger.warning("\n\tSKIPPING example: {}\n\tas 'latest' is in its unsupported profiles."
"\n\t\tunsupported-profiles: {}.".format(example['text'], example['unsupported-profiles']))
continue
example_text = example.get('text','')
commands = _extract_commands_from_example(example_text)
while commands:
command = commands.pop()
violation, nested_commands = _lint_example_command(command, parser)
commands.extend(nested_commands) # append commands that are the source of any arguments
if violation:
violations.append(violation)
if violations:
num_err = len(violations)
violation_str = "\n\n".join(violations)
violation_msg = "\n\tThere is a violation:\n{}.".format(violation_str) if num_err == 1 else \
"\n\tThere are {} violations:\n{}".format(num_err, violation_str)
raise RuleError(violation_msg + "\n\n")
### Faulty help example parameters rule helpers
@mock.patch("azure.cli.core.parser.AzCliCommandParser._check_value")
@mock.patch("argparse.ArgumentParser._get_value")
@mock.patch("azure.cli.core.parser.AzCliCommandParser.error")
def _lint_example_command(command, parser, mocked_error_method, mocked_get_value, mocked_check_value):
def get_value_side_effect(action, arg_string):
return arg_string
mocked_error_method.side_effect = LinterError # mock call of parser.error so usage won't be printed.
mocked_get_value.side_effect = get_value_side_effect
violation = None
nested_commands = []
try:
command_args = shlex.split(command, comments=True)[1:] # split commands into command args, ignore comments.
command_args, nested_commands = _process_command_args(command_args)
parser.parse_args(command_args)
except ValueError as e: # handle exception thrown by shlex.
if str(e) == "No closing quotation":
violation = '\t"{}"\n\thas no closing quotation. Tip: to continue an example ' \
'command on the next line, use a "\\" followed by a newline.\n\t' \
'If needed, you can escape the "\\", like so "\\\\"'.format(command)
else:
raise e
except LinterError: # handle parsing failure due to invalid option
violation = '\t"{}" is not a valid command'.format(command)
if mocked_error_method.called:
call_args = mocked_error_method.call_args
violation = "{}.\n\t{}".format(violation, call_args[0][0])
return violation, nested_commands
# return list of commands in the example text
def _extract_commands_from_example(example_text):
# fold commands spanning multiple lines into one line. Split commands that use pipes
# handle single and double quotes properly
lines = example_text.splitlines()
example_text = ""
quote = None
for line in lines:
for ch in line:
if quote is None:
if ch == '"' or ch == "'":
quote = ch
elif ch == quote:
quote = None
if quote is None and line.endswith("\\"):
# attach this line with removed '\' and no '\n' (space at the end to keep consistent with initial algorithm)
example_text += line[0:-1] + " "
elif quote is not None:
# attach this line without '\n'
example_text += line
else:
# attach this line with '\n' as no quote and no continuation
example_text += line + "\n"
# this is also for consistency with original algorithm
example_text = example_text.replace("\\ ", " ")
commands = example_text.splitlines()
processed_commands = []
for command in commands: # filter out commands
command.strip()
if command.startswith("az"): # if this is a single az command add it.
processed_commands.append(command)
for re_prog in [_CMD_SUB_1, _CMD_SUB_2]:
start = 0
match = re_prog.search(command, start)
while match: # while there is a nested az command of type 1 $( az ...)
processed_commands.append(match.group(1).strip()) # add it
start = match.end(1) # get index of rest of string
match = re_prog.search(command, start) # attempt to get next match
return processed_commands
def _process_command_args(command_args):
result_args = []
new_commands = []
operators = ["&&","||", "|"]
for arg in command_args: # strip unnecessary punctuation, otherwise arg validation could fail.
if arg in operators: # handle cases where multiple commands are connected by control operators or pipe.
idx = command_args.index(arg)
maybe_new_command = " ".join(command_args[idx:])
idx = maybe_new_command.find("az ")
if idx != -1:
new_commands.append(maybe_new_command[idx:]) # remaining command is in fact a new command / commands.
break
result_args.append(arg)
return result_args, new_commands
|