azdev/operations/linter/__init__.py (141 lines of code) (raw):

# ----------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for # license information. # ----------------------------------------------------------------------------- import os import sys import time import yaml from knack.help_files import helps from knack.log import get_logger from knack.util import CLIError from azdev.utilities import ( heading, subheading, display, get_path_table, require_azure_cli, filter_by_git_diff) from azdev.utilities.path import get_cli_repo_path, get_ext_repo_paths from azdev.operations.style import run_pylint from .linter import LinterManager, LinterScope, RuleError, LinterSeverity from .util import filter_modules, merge_exclusion logger = get_logger(__name__) CHECKERS_PATH = 'azdev.operations.linter.pylint_checkers' # pylint:disable=too-many-locals, too-many-statements, too-many-branches def run_linter(modules=None, rule_types=None, rules=None, ci_exclusions=None, git_source=None, git_target=None, git_repo=None, include_whl_extensions=False, min_severity=None, save_global_exclusion=False): require_azure_cli() from azure.cli.core import get_default_cli # pylint: disable=import-error from azure.cli.core.file_util import ( # pylint: disable=import-error get_all_help, create_invoker_and_load_cmds_and_args) heading('CLI Linter') # allow user to run only on CLI or extensions cli_only = modules == ['CLI'] ext_only = modules == ['EXT'] if cli_only or ext_only: modules = None # process severity option if min_severity: try: min_severity = LinterSeverity.get_linter_severity(min_severity) except ValueError: valid_choices = linter_severity_choices() raise CLIError("Please specify a valid linter severity. It should be one of: {}" .format(", ".join(valid_choices))) # needed to remove helps from azdev azdev_helps = helps.copy() exclusions = {} selected_modules = get_path_table(include_only=modules, include_whl_extensions=include_whl_extensions) if cli_only: selected_modules['ext'] = {} if ext_only: selected_modules['mod'] = {} selected_modules['core'] = {} # used to upsert global exclusion update_global_exclusion = None if save_global_exclusion and (cli_only or ext_only): if cli_only: update_global_exclusion = 'CLI' if os.path.exists(os.path.join(get_cli_repo_path(), 'linter_exclusions.yml')): os.remove(os.path.join(get_cli_repo_path(), 'linter_exclusions.yml')) elif ext_only: update_global_exclusion = 'EXT' for ext_path in get_ext_repo_paths(): if os.path.exists(os.path.join(ext_path, 'linter_exclusions.yml')): os.remove(os.path.join(ext_path, 'linter_exclusions.yml')) # filter down to only modules that have changed based on git diff selected_modules = filter_by_git_diff(selected_modules, git_source, git_target, git_repo) if not any(selected_modules.values()): logger.warning('No commands selected to check.') selected_mod_names = list(selected_modules['mod'].keys()) + list(selected_modules['core'].keys()) + \ list(selected_modules['ext'].keys()) selected_mod_paths = list(selected_modules['mod'].values()) + list(selected_modules['core'].values()) + \ list(selected_modules['ext'].values()) if selected_mod_names: display('Modules: {}\n'.format(', '.join(selected_mod_names))) # collect all rule exclusions for path in selected_mod_paths: exclusion_path = os.path.join(path, 'linter_exclusions.yml') if os.path.isfile(exclusion_path): with open(exclusion_path) as f: mod_exclusions = yaml.safe_load(f) merge_exclusion(exclusions, mod_exclusions or {}) global_exclusion_paths = [os.path.join(get_cli_repo_path(), 'linter_exclusions.yml')] try: global_exclusion_paths.extend([os.path.join(path, 'linter_exclusions.yml') for path in (get_ext_repo_paths() or [])]) except CLIError: pass for path in global_exclusion_paths: if os.path.isfile(path): with open(path) as f: mod_exclusions = yaml.safe_load(f) merge_exclusion(exclusions, mod_exclusions or {}) start = time.time() display('Initializing linter with command table and help files...') az_cli = get_default_cli() # load commands, args, and help create_invoker_and_load_cmds_and_args(az_cli) loaded_help = get_all_help(az_cli) stop = time.time() logger.info('Commands and help loaded in %i sec', stop - start) command_loader = az_cli.invocation.commands_loader # format loaded help loaded_help = {data.command: data for data in loaded_help if data.command} # load yaml help help_file_entries = {} for entry_name, help_yaml in helps.items(): # ignore help entries from azdev itself, unless it also coincides # with a CLI or extension command name. if entry_name in azdev_helps and entry_name not in command_loader.command_table: continue help_entry = yaml.safe_load(help_yaml) help_file_entries[entry_name] = help_entry # trim command table and help to just selected_modules command_loader, help_file_entries = filter_modules( command_loader, help_file_entries, modules=selected_mod_names, include_whl_extensions=include_whl_extensions) if not command_loader.command_table: logger.warning('No commands selected to check.') # Instantiate and run Linter linter_manager = LinterManager(command_loader=command_loader, help_file_entries=help_file_entries, loaded_help=loaded_help, exclusions=exclusions, rule_inclusions=rules, use_ci_exclusions=ci_exclusions, min_severity=min_severity, update_global_exclusion=update_global_exclusion, git_source=git_source, git_target=git_target, git_repo=git_repo) subheading('Results') logger.info('Running linter: %i commands, %i help entries', len(command_loader.command_table), len(help_file_entries)) exit_code = linter_manager.run( run_params=not rule_types or 'params' in rule_types, run_commands=not rule_types or 'commands' in rule_types, run_command_groups=not rule_types or 'command_groups' in rule_types, run_help_files_entries=not rule_types or 'help_entries' in rule_types, run_command_test_coverage=not rule_types or 'command_test_coverage' in rule_types, ) display(os.linesep + 'Run custom pylint rules.') exit_code += pylint_rules(selected_modules) print(exit_code) sys.exit(exit_code) def pylint_rules(selected_modules): # TODO: support severity for pylint rules from importlib import import_module my_env = os.environ.copy() checker_path = import_module('{}'.format(CHECKERS_PATH)).__path__[0] my_env['PYTHONPATH'] = checker_path checkers = [os.path.splitext(f)[0] for f in os.listdir(checker_path) if os.path.isfile(os.path.join(checker_path, f)) and f != '__init__.py'] enable = [s.replace('_', '-') for s in checkers] pylint_result = run_pylint(selected_modules, env=my_env, checkers=checkers, disable_all=True, enable=enable) if pylint_result and not pylint_result.error: display(os.linesep + 'No violations found for custom pylint rules.') display('Linter: PASSED\n') if pylint_result and pylint_result.error: display(pylint_result.error.output.decode('utf-8')) display('Linter: FAILED\n') return pylint_result.exit_code def linter_severity_choices(): return [str(severity.name).lower() for severity in LinterSeverity]