File: help_rules.py

package info (click to toggle)
azure-cli 2.82.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,359,416 kB
  • sloc: python: 1,910,381; sh: 1,343; makefile: 406; cs: 145; javascript: 74; sql: 37; xml: 21
file content (192 lines) | stat: -rw-r--r-- 8,393 bytes parent folder | download | duplicates (3)
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