Skip to content

Commit 381d1f7

Browse files
authored
add example missing check (#500)
* add example missing check
1 parent babeb53 commit 381d1f7

7 files changed

Lines changed: 152 additions & 4 deletions

File tree

azdev/operations/constant.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# license information.
55
# -----------------------------------------------------------------------------
66
# pylint: disable=line-too-long
7-
7+
import os
88
ENCODING = 'utf-8'
99

1010
# Base on https://github.com/Azure/azure-cli/blob/dev/.github/CODEOWNERS
@@ -259,3 +259,8 @@
259259
PREVIEW_INIT_SUFFIX = "b1"
260260

261261
CLI_EXTENSION_INDEX_URL = "https://azcliextensionsync.blob.core.windows.net/index1/index.json"
262+
263+
CMD_EXAMPLE_CONFIG_FILE = "./data/cmd_example_config.json"
264+
CMD_EXAMPLE_CONFIG_FILE_PATH = f"{os.path.dirname(os.path.realpath(__file__))}/linter/{CMD_EXAMPLE_CONFIG_FILE}"
265+
CMD_EXAMPLE_CONFIG_FILE_URL = "https://azcmdchangemgmt.blob.core.windows.net/azure-cli-dev-tool-config/cmd_example_config.json"
266+
CMD_EXAMPLE_DEFAULT = 1
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"create": 1,
3+
"add": 1,
4+
"update": 1,
5+
"list": 0,
6+
"delete": 0,
7+
"remove": 0,
8+
"show": 0,
9+
"wait": 0
10+
}

azdev/operations/linter/linter.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# -----------------------------------------------------------------------------
6-
6+
# pylint: disable=line-too-long
77
from difflib import context_diff
88
from enum import Enum
99
from importlib import import_module
@@ -17,14 +17,16 @@
1717

1818
from azdev.operations.regex import (
1919
get_all_tested_commands_from_regex,
20+
search_aaz_raw_command, search_aaz_custom_command,
2021
search_argument,
2122
search_argument_context,
2223
search_command,
2324
search_deleted_command,
2425
search_command_group)
2526
from azdev.utilities import diff_branches_detail, diff_branch_file_patch
2627
from azdev.utilities.path import get_cli_repo_path, get_ext_repo_paths
27-
from .util import share_element, exclude_commands, LinterError
28+
from .util import (share_element, exclude_commands, LinterError, get_cmd_example_configurations,
29+
get_cmd_example_threshold)
2830

2931
PACKAGE_NAME = 'azdev.operations.linter'
3032
_logger = get_logger(__name__)
@@ -224,6 +226,30 @@ def get_parameter_test_coverage(self):
224226
all_tested_command = self._detect_tested_command(diff_index)
225227
return self._run_parameter_test_coverage(parameters, all_tested_command)
226228

229+
def check_missing_command_example(self):
230+
_exclude_commands = self._get_cmd_exclusions(rule_name="missing_command_example")
231+
cmd_example_config = get_cmd_example_configurations()
232+
commands = self._detect_modified_command()
233+
violations = []
234+
for cmd in commands:
235+
if cmd in _exclude_commands:
236+
continue
237+
cmd_help = self._loaded_help.get(cmd, None)
238+
if not cmd_help:
239+
continue
240+
# parameters = cmd_help.parameters
241+
# add if future parameter set required
242+
cmd_suffix = cmd.split()[-1]
243+
cmd_example_threshold = get_cmd_example_threshold(cmd_suffix, cmd_example_config)
244+
if cmd_example_threshold == 0:
245+
continue
246+
if not hasattr(cmd_help, "examples") or len(cmd_help.examples) < cmd_example_threshold:
247+
violations.append(f'Command `{cmd}` should have at least {cmd_example_threshold} example(s)')
248+
if violations:
249+
violations.insert(0, 'Check command example failed.')
250+
violations.extend(['Please add examples for the modified command or add the command in rule_exclusions: missing_command_example in linter_exclusions.yml'])
251+
return violations
252+
227253
def _get_exclusions(self):
228254
_exclude_commands = set()
229255
_exclude_parameters = set()
@@ -238,6 +264,16 @@ def _get_exclusions(self):
238264
_logger.debug('exclude_comands: %s', _exclude_commands)
239265
return _exclude_commands, _exclude_parameters
240266

267+
def _get_cmd_exclusions(self, rule_name=None):
268+
_exclude_commands = set()
269+
if not rule_name:
270+
return _exclude_commands
271+
for command, details in self.exclusions.items():
272+
if 'rule_exclusions' in details and rule_name in details['rule_exclusions']:
273+
_exclude_commands.add(command)
274+
_logger.debug('exclude_commands: %s', _exclude_commands)
275+
return _exclude_commands
276+
241277
def _split_path(self, path: str):
242278
parts = path.rsplit('/', maxsplit=1)
243279
return parts if len(parts) == 2 else ('', parts[0])
@@ -387,6 +423,40 @@ def _run_parameter_test_coverage(parameters, all_tested_command):
387423
'Or add the parameter with missing_parameter_test_coverage rule in linter_exclusions.yml'])
388424
return exec_state, violations
389425

426+
def _detect_modified_command(self):
427+
modified_commands = set()
428+
diff_patches = diff_branch_file_patch(repo=self.git_repo, target=self.git_target, source=self.git_source)
429+
for change in diff_patches:
430+
file_path, filename = self._split_path(change.a_path)
431+
if "commands.py" not in filename and "aaz" not in file_path:
432+
continue
433+
current_lines = self._read_blob_lines(change.b_blob)
434+
patch = change.diff.decode("utf-8")
435+
patch_lines = patch.splitlines()
436+
if 'commands.py' in filename:
437+
added_lines = [line for line in patch_lines if line.startswith('+') and not line.startswith('+++')]
438+
for line in added_lines:
439+
if aaz_custom_command := search_aaz_custom_command(line):
440+
modified_commands.add(aaz_custom_command)
441+
442+
for row_num, line in enumerate(patch_lines):
443+
if not line.startswith("+") or line.startswith('+++'):
444+
continue
445+
manual_command_suffix = search_command(line)
446+
if manual_command_suffix:
447+
idx = self._get_line_number(patch_lines, row_num, r'@@ -(\d+),(?:\d+) \+(?:\d+),(?:\d+) @@')
448+
manual_command = search_command_group(idx, current_lines, manual_command_suffix)
449+
if manual_command:
450+
modified_commands.add(manual_command)
451+
452+
if "aaz" in file_path:
453+
if aaz_raw_command := search_aaz_raw_command(patch):
454+
modified_commands.add(aaz_raw_command)
455+
456+
commands = list(modified_commands)
457+
_logger.debug('Modified commands: %s', modified_commands)
458+
return commands
459+
390460
def _get_diffed_patches(self):
391461
if not self.git_source or not self.git_target or not self.git_repo:
392462
return

azdev/operations/linter/rules/command_coverage_rules.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,11 @@ def missing_parameter_test_coverage(linter):
2222
if not exec_state:
2323
violation_msg = "\n\t".join(violations)
2424
raise RuleError(violation_msg + "\n")
25+
26+
27+
@CommandCoverageRule(LinterSeverity.HIGH)
28+
def missing_command_example(linter):
29+
violations = linter.check_missing_command_example()
30+
if violations:
31+
violation_msg = "\n\t".join(violations)
32+
raise RuleError(violation_msg + "\n")

azdev/operations/linter/util.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66

77
import copy
88
import re
9+
import os
10+
import json
911
import requests
1012

1113
from knack.log import get_logger
1214

1315
from azdev.utilities import get_name_index
14-
from azdev.operations.constant import ALLOWED_HTML_TAG
16+
from azdev.operations.constant import (ALLOWED_HTML_TAG, CMD_EXAMPLE_CONFIG_FILE_URL,
17+
CMD_EXAMPLE_CONFIG_FILE_PATH, CMD_EXAMPLE_DEFAULT)
1518

1619

1720
logger = get_logger(__name__)
@@ -155,3 +158,26 @@ def has_broken_site_links(help_message, filtered_lines=None):
155158
if filtered_lines:
156159
invalid_urls = [s for s in invalid_urls if any(s in diff_line for diff_line in filtered_lines)]
157160
return invalid_urls
161+
162+
163+
def get_cmd_example_configurations():
164+
cmd_example_threshold = {}
165+
remote_res = requests.get(CMD_EXAMPLE_CONFIG_FILE_URL)
166+
if remote_res.status_code != 200:
167+
logger.warning("remote cmd example configuration fetch error, use local dict")
168+
if not os.path.exists(CMD_EXAMPLE_CONFIG_FILE_PATH):
169+
logger.info("cmd_example_config.json not exist, skipped")
170+
return cmd_example_threshold
171+
with open(CMD_EXAMPLE_CONFIG_FILE_PATH, "r") as f_in:
172+
cmd_example_threshold = json.load(f_in)
173+
else:
174+
logger.info("remote cmd example configuration fetch success")
175+
cmd_example_threshold = remote_res.json()
176+
return cmd_example_threshold
177+
178+
179+
def get_cmd_example_threshold(cmd_suffix, cmd_example_config):
180+
for cmd_type, threshold in cmd_example_config.items():
181+
if cmd_suffix.find(cmd_type) != -1:
182+
return threshold
183+
return CMD_EXAMPLE_DEFAULT

azdev/operations/regex.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,31 @@ def search_deleted_command(line):
173173
if ref:
174174
command = ref[0].split(',')[0].strip("'")
175175
return command
176+
177+
178+
def search_aaz_custom_command(line):
179+
"""
180+
re match pattern
181+
+ self.command_table['monitor autoscale update'] = AutoScaleUpdate(loader=self)
182+
"""
183+
cmd = ''
184+
aaz_custom_cmd_pattern = r"\+.*\.command_table\[['\"](.*?)['\"]\]"
185+
ref = re.findall(aaz_custom_cmd_pattern, line)
186+
if ref:
187+
cmd = ref[0].strip()
188+
return cmd
189+
190+
191+
def search_aaz_raw_command(lines):
192+
"""
193+
re match pattern
194+
+@register_command(
195+
+ "monitor autoscale update",
196+
+)
197+
"""
198+
cmd = ''
199+
aaz_raw_cmd_pattern = r"\+@register_command\([\s\S]*?\+.*?['\"](.*?)['\"]"
200+
ref = re.findall(aaz_raw_cmd_pattern, str(lines))
201+
if ref:
202+
cmd = ref[0].strip()
203+
return cmd

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
'azdev.config': ['*.*', 'cli_pylintrc', 'ext_pylintrc'],
9494
'azdev.mod_templates': ['*.*'],
9595
'azdev.operations.linter.rules': ['ci_exclusions.yml'],
96+
'azdev.operations.linter': ["data/*"],
9697
'azdev.operations.cmdcov': ['*.*'],
9798
'azdev.operations.breaking_change': ['*.*'],
9899
},

0 commit comments

Comments
 (0)