def test_argument_file_without_path(self): config = Config() with pytest.raises(ArgumentFileNotFoundError) as err: config.parse_opts(['--argumentfile']) assert 'Argument file "" does not exist' in str(err)
def setUp(self): self.config = Config()
def test_run_all_checkers(self, robocop_instance): config = Config() config.parse_opts([str(Path(Path(__file__).parent.parent, 'test_data'))]) robocop_instance.config = config with pytest.raises(SystemExit): robocop_instance.run()
def test_use_not_existing_argument_file(self): config = Config() with pytest.raises(ArgumentFileNotFoundError) as err: config.parse_opts(['--argumentfile', 'some_file', str(Path(Path(__file__).parent.parent, 'test_data'))]) assert 'Argument file "some_file" does not exist' in str(err)
class Robocop: """ Main class for running the checks. If you want to run checks with non default configuration create your own ``Config`` and pass it to ``Robocop``. Use ``Robocop.run()`` method to start analysis. If ``from_cli`` is set to ``False`` it will return list of found issues in JSON format. Example:: import robocop from robocop.config import Config config = Config() config.include = {'1003'} config.paths = ['tests\\atest\\rules\\section-out-of-order'] robocop_runner = robocop.Robocop(config=config) issues = robocop_runner.run() """ def __init__(self, from_cli=False, config=None): self.files = {} self.checkers = [] self.rules = {} self.reports = dict() self.disabler = None self.root = os.getcwd() self.config = Config() if config is None else config self.from_cli = from_cli if from_cli: self.config.parse_opts() else: self.config.reports.add('json_report') self.out = self.set_output() def set_output(self): """ Set output for printing to file if configured. Else use standard output """ return self.config.output or None def write_line(self, line): """ Print line using file=self.out parameter (set in `set_output` method) """ print(line, file=self.out) def reload_config(self): """ Reload checkers and reports based on current config """ self.load_checkers() self.list_checkers() self.load_reports() self.configure_checkers_or_reports() def run(self): """ Entry point for running scans """ self.reload_config() self.recognize_file_types() self.run_checks() self.make_reports() if self.config.output and not self.out.closed: self.out.close() if self.from_cli: sys.exit(self.reports['return_status'].return_status) else: return self.reports['json_report'].issues def recognize_file_types(self): """ Pre-parse files to recognize their types. If the filename is `__init__.*`, the type is `INIT`. Files with .resource extension are `RESOURCE` type. If the file is imported somewhere then file type is `RESOURCE`. Otherwise file type is `GENERAL`. These types are important since they are used to define parsing class for robot API. """ files = self.config.paths for file in self.get_files(files, self.config.recursive): if '__init__' in file.name: self.files[file] = FileType.INIT elif file.suffix.lower() == '.resource': self.files[file] = FileType.RESOURCE else: self.files[file] = FileType.GENERAL file_type_checker = FileTypeChecker(self.files, self.config.exec_dir) for file in self.files: file_type_checker.source = file model = get_model(file) file_type_checker.visit(model) def run_checks(self): for file in self.files: found_issues = [] self.register_disablers(file) if self.disabler.file_disabled: continue model = self.files[file].get_parser()(str(file)) for checker in self.checkers: if checker.disabled: continue checker.source = str(file) checker.scan_file(model) found_issues += checker.issues checker.issues.clear() found_issues.sort() for issue in found_issues: self.report(issue) def register_disablers(self, file): """ Parse content of file to find any disabler statements like # robocop: disable=rulename """ self.disabler = DisablersFinder(file, self) def report(self, rule_msg): if not rule_msg.enabled: # disabled from cli return if self.disabler.is_rule_disabled( rule_msg): # disabled from source code return for report in self.reports.values(): report.add_message(rule_msg) try: source_rel = os.path.relpath(os.path.expanduser(rule_msg.source), self.root) except ValueError: source_rel = rule_msg.source self.log_message(source=rule_msg.source, source_rel=source_rel, line=rule_msg.line, col=rule_msg.col, severity=rule_msg.severity.value, rule_id=rule_msg.rule_id, desc=rule_msg.desc, msg_name=rule_msg.name) def log_message(self, **kwargs): self.write_line(self.config.format.format(**kwargs)) def load_checkers(self): self.checkers = [] self.rules = {} checkers.init(self) def list_checkers(self): if not (self.config.list or self.config.list_configurables): return rule_by_id = { msg.rule_id: msg for checker in self.checkers for msg in checker.rules_map.values() } rule_ids = sorted([key for key in rule_by_id]) for rule_id in rule_ids: if self.config.list: if not rule_by_id[rule_id].matches_pattern(self.config.list): continue print(rule_by_id[rule_id]) else: if not rule_by_id[rule_id].matches_pattern( self.config.list_configurables): continue print( f"{rule_by_id[rule_id]}\n {rule_by_id[rule_id].available_configurables()}" ) sys.exit() def load_reports(self): self.reports = dict() classes = inspect.getmembers(reports, inspect.isclass) available_reports = 'Available reports:\n' for report_class in classes: if not issubclass(report_class[1], reports.Report): continue report = report_class[1]() if not hasattr(report, 'name'): continue if 'all' in self.config.reports or report.name in self.config.reports: self.reports[report.name] = report available_reports += f'{report.name} - {report.description}\n' if self.config.list_reports: available_reports += 'all - Turns on all available reports' print(available_reports) sys.exit() def register_checker(self, checker): if not self.any_rule_enabled(checker): checker.disabled = True for rule_name, rule in checker.rules_map.items(): if rule_name in self.rules: (_, checker_prev) = self.rules[rule_name] raise robocop.exceptions.DuplicatedRuleError( 'name', rule_name, checker, checker_prev) if rule.rule_id in self.rules: (_, checker_prev) = self.rules[rule.rule_id] raise robocop.exceptions.DuplicatedRuleError( 'id', rule.rule_id, checker, checker_prev) self.rules[rule_name] = (rule, checker) self.rules[rule.rule_id] = (rule, checker) self.checkers.append(checker) def make_reports(self): for report in self.reports.values(): output = report.get_report() if output is not None: self.write_line(output) def get_files(self, files_or_dirs, recursive): for file in files_or_dirs: yield from self.get_absolute_path(Path(file), recursive) def get_absolute_path(self, path, recursive): if not path.exists(): raise robocop.exceptions.FileError(path) if self.config.is_path_ignored(path): return if path.is_file(): if self.should_parse(path): yield path.absolute() elif path.is_dir(): for file in path.iterdir(): if file.is_dir() and not recursive: continue yield from self.get_absolute_path(file, recursive) def should_parse(self, file): """ Check if file extension is in list of supported file types (can be configured from cli) """ return file.suffix and file.suffix.lower() in self.config.filetypes def any_rule_enabled(self, checker): for name, rule in checker.rules_map.items(): rule.enabled = self.config.is_rule_enabled(rule) checker.rules_map[name] = rule return any(msg.enabled for msg in checker.rules_map.values()) def configure_checkers_or_reports(self): for config in self.config.configure: if config.count(':') < 2: raise robocop.exceptions.ConfigGeneralError( f"Provided invalid config: '{config}' (general pattern: <rule>:<param>:<value>)" ) rule_or_report, param, value, *values = config.split(':') if rule_or_report in self.rules: msg, checker = self.rules[rule_or_report] if param == 'severity': self.rules[rule_or_report] = (msg.change_severity(value), checker) else: configurable = msg.get_configurable(param) if configurable is None: available_conf = msg.available_configurables() raise robocop.exceptions.ConfigGeneralError( f"Provided param '{param}' for rule '{rule_or_report}' does not exist. {available_conf}" ) checker.configure(configurable[1], configurable[2](value)) elif rule_or_report in self.reports: self.reports[rule_or_report].configure(param, value, *values) else: raise robocop.exceptions.ConfigGeneralError( f"Provided rule or report '{rule_or_report}' does not exist" )
def test_paths_from_gitignore_ignored(self): test_dir = Path(__file__).parent.parent / "test_data" / "gitignore" config = Config() config.paths = {str(test_dir)} files = list(get_files(config)) assert sorted(files) == [test_dir / "allowed" / "allowed_file.robot", test_dir / "allowed_file.robot"]
def test_invalid_config(self): config_path = Path(Path(__file__).parent.parent, "test_data", "api_invalid_config") with pytest.raises(InvalidArgumentError) as exception: Config(root=config_path) assert r"Invalid configuration for Robocop:\nunrecognized arguments: --some" in str(exception)
def get_message_with_id(rule_id): rule_id = Config.replace_severity_values(rule_id) return Rule(rule_id=rule_id, name=f"some-message-{rule_id}", msg="Some description", severity=RuleSeverity.WARNING)
def config(): return Config()
class Robocop: """ Main class for running the checks. If you want to run checks with non default configuration create your own ``Config`` and pass it to ``Robocop``. Use ``Robocop.run()`` method to start analysis. If ``from_cli`` is set to ``False`` it will return list of found issues in JSON format. Example:: import robocop from robocop.config import Config config = Config() config.include = {'1003'} config.paths = ['tests\\atest\\rules\\section-out-of-order'] robocop_runner = robocop.Robocop(config=config) issues = robocop_runner.run() """ def __init__(self, from_cli: bool = False, config: Config = None): self.files = {} self.checkers = [] self.rules = {} self.reports = {} self.disabler = None self.root = os.getcwd() self.config = Config(from_cli=from_cli) if config is None else config self.from_cli = from_cli if not from_cli: self.config.reports.add("json_report") self.out = self.set_output() def set_output(self): """Set output for printing to file if configured. Else use standard output""" return self.config.output or None def write_line(self, line): """Print line using file=self.out parameter (set in `set_output` method)""" print(line, file=self.out) def reload_config(self): """Reload checkers and reports based on current config""" self.load_checkers() self.config.validate_rule_names(self.rules) self.load_reports() self.configure_checkers_or_reports() self.check_for_disabled_rules() self.list_checkers() def run(self): """Entry point for running scans""" self.reload_config() self.recognize_file_types() self.run_checks() self.make_reports() if self.config.output and not self.out.closed: self.out.close() if self.from_cli: sys.exit(self.reports["return_status"].return_status) else: return self.reports["json_report"].issues def recognize_file_types(self): """ Pre-parse files to recognize their types. If the filename is `__init__.*`, the type is `INIT`. Files with .resource extension are `RESOURCE` type. If the file is imported somewhere then file type is `RESOURCE`. Otherwise, file type is `GENERAL`. These types are important since they are used to define parsing class for robot API. """ file_type_checker = FileTypeChecker(self.config.exec_dir) for file in get_files(self.config): if "__init__" in file.name: file_type = FileType.INIT elif file.suffix.lower() == ".resource": file_type = FileType.RESOURCE else: file_type = FileType.GENERAL file_type_checker.source = file try: model = file_type.get_parser()(str(file)) file_type_checker.visit(model) self.files[file] = (file_type, model) except DataError: print( f"Failed to decode {file}. Default supported encoding by Robot Framework is UTF-8. Skipping file" ) for resource in file_type_checker.resource_files: if resource in self.files and self.files[resource][ 0].value != FileType.RESOURCE: self.files[resource] = ( FileType.RESOURCE, get_resource_model(str(resource)), ) def run_checks(self): for file in self.files: if self.config.verbose: print(f"Scanning file: {file}") model = self.files[file][1] found_issues = self.run_check(model, str(file)) found_issues.sort() for issue in found_issues: self.report(issue) if "file_stats" in self.reports: self.reports["file_stats"].files_count = len(self.files) def run_check(self, ast_model, filename, source=None): found_issues = [] self.register_disablers(filename, source) if self.disabler.file_disabled: return [] templated = is_suite_templated(ast_model) for checker in self.checkers: if checker.disabled: continue found_issues += [ issue for issue in checker.scan_file(ast_model, filename, source, templated) if not self.disabler.is_rule_disabled(issue) ] return found_issues def register_disablers(self, filename, source): """Parse content of file to find any disabler statements like # robocop: disable=rulename""" self.disabler = DisablersFinder(filename=filename, source=source) def report(self, rule_msg: Message): for report in self.reports.values(): report.add_message(rule_msg) try: source_rel = os.path.relpath(os.path.expanduser(rule_msg.source), self.root) except ValueError: source_rel = rule_msg.source self.log_message( source=rule_msg.source, source_rel=source_rel, line=rule_msg.line, col=rule_msg.col, end_line=rule_msg.end_line, end_col=rule_msg.end_col, severity=rule_msg.severity.value, rule_id=rule_msg.rule_id, desc=rule_msg.desc, name=rule_msg.name, ) def log_message(self, **kwargs): self.write_line(self.config.format.format(**kwargs)) def load_checkers(self): self.checkers = [] self.rules = {} checkers.init(self) def list_checkers(self): if not (self.config.list or self.config.list_configurables): return if self.config.list_configurables: print( "All rules have configurable parameter 'severity'. Allowed values are:" "\n E / error\n W / warning\n I / info\n") pattern = self.config.list if self.config.list else self.config.list_configurables rule_by_id = { rule.rule_id: rule for rule in self.rules.values() if rule.matches_pattern(pattern) } rule_by_id = sorted(rule_by_id.values(), key=lambda x: x.rule_id) severity_counter = Counter({"E": 0, "W": 0, "I": 0}) for rule in rule_by_id: if self.config.list: print(rule) severity_counter[rule.severity.value] += 1 else: _, params = rule.available_configurables( include_severity=False) if params: print(f"{rule}\n" f" {params}") severity_counter[rule.severity.value] += 1 configurable_rules_sum = sum(severity_counter.values()) plural = "" if configurable_rules_sum == 1 else "s" print( f"\nAltogether {configurable_rules_sum} rule{plural} with following severity:\n" f" {severity_counter['E']} error rule{'' if severity_counter['E'] == 1 else 's'},\n" f" {severity_counter['W']} warning rule{'' if severity_counter['W'] == 1 else 's'},\n" f" {severity_counter['I']} info rule{'' if severity_counter['I'] == 1 else 's'}.\n" ) print( "Visit https://robocop.readthedocs.io/en/stable/rules.html page for detailed documentation." ) sys.exit() def load_reports(self): self.reports = {} classes = inspect.getmembers(reports, inspect.isclass) available_reports = "Available reports:\n" for report_class in classes: if not issubclass(report_class[1], reports.Report): continue report = report_class[1]() if not hasattr(report, "name"): continue if "all" in self.config.reports or report.name in self.config.reports: self.reports[report.name] = report available_reports += f"{report.name:20} - {report.description}\n" if self.config.list_reports: available_reports += "all" + " " * 18 + "- Turns on all available reports" print(available_reports) sys.exit() def register_checker(self, checker): for rule_name, rule in checker.rules.items(): self.rules[rule_name] = rule self.rules[rule.rule_id] = rule self.checkers.append(checker) def check_for_disabled_rules(self): """Check checker configuration to disable rules.""" for checker in self.checkers: if not self.any_rule_enabled(checker): checker.disabled = True def make_reports(self): for report in self.reports.values(): output = report.get_report() if output is not None: self.write_line(output) def any_rule_enabled(self, checker) -> bool: for name, rule in checker.rules.items(): rule.enabled = self.config.is_rule_enabled(rule) checker.rules[name] = rule return any(msg.enabled for msg in checker.rules.values()) def configure_checkers_or_reports(self): for config in self.config.configure: if config.count(":") < 2: raise robocop.exceptions.ConfigGeneralError( f"Provided invalid config: '{config}' (general pattern: <rule>:<param>:<value>)" ) rule_or_report, param, value = config.split(":", maxsplit=2) if rule_or_report in self.rules: rule = self.rules[rule_or_report] rule.configure(param, value) elif rule_or_report in self.reports: self.reports[rule_or_report].configure(param, value) else: similar = RecommendationFinder().find_similar( rule_or_report, self.rules) raise robocop.exceptions.ConfigGeneralError( f"Provided rule or report '{rule_or_report}' does not exist. {similar}" )
def test_set_rule_threshold(self, threshold, robocop_instance, test_data_dir): with mock.patch.object(sys, "argv", f"robocop --threshold {threshold}".split()): Config(from_cli=True)
def test_use_argument_file(self, robocop_instance, test_data_dir): config = Config() config.parse_opts( ["-A", str(test_data_dir / "argument_file" / "args.txt")])
class Robocop: """ Main class for running the checks. If you want to run checks with non default configuration create your own ``Config`` and pass it to ``Robocop``. Use ``Robocop.run()`` method to start analysis. If ``from_cli`` is set to ``False`` it will return list of found issues in JSON format. Example:: import robocop from robocop.config import Config config = Config() config.include = {'1003'} config.paths = ['tests\\atest\\rules\\section-out-of-order'] robocop_runner = robocop.Robocop(config=config) issues = robocop_runner.run() """ def __init__(self, from_cli=False, config=None): self.files = {} self.checkers = [] self.rules = {} self.reports = dict() self.disabler = None self.root = os.getcwd() self.config = Config(from_cli=from_cli) if config is None else config self.from_cli = from_cli self.config.parse_opts(from_cli=from_cli) if not from_cli: self.config.reports.add('json_report') self.out = self.set_output() if from_cli: print( "### DEPRECATION WARNING: The rule '0906' (redundant-equal-sign) is " "deprecated starting from Robocop 1.7.0 and is replaced by 0909 (inconsistent-assignment) and " "0910 (inconsistent-assignment-in-variables). " "Rule '0906' will be removed in the next release - update your configuration. ###\n" ) def set_output(self): """ Set output for printing to file if configured. Else use standard output """ return self.config.output or None def write_line(self, line): """ Print line using file=self.out parameter (set in `set_output` method) """ print(line, file=self.out) def reload_config(self): """ Reload checkers and reports based on current config """ self.load_checkers() self.config.validate_rule_names(self.rules) self.list_checkers() self.load_reports() self.configure_checkers_or_reports() def run(self): """ Entry point for running scans """ self.reload_config() self.recognize_file_types() self.run_checks() self.make_reports() if self.config.output and not self.out.closed: self.out.close() if self.from_cli: sys.exit(self.reports['return_status'].return_status) else: return self.reports['json_report'].issues def recognize_file_types(self): """ Pre-parse files to recognize their types. If the filename is `__init__.*`, the type is `INIT`. Files with .resource extension are `RESOURCE` type. If the file is imported somewhere then file type is `RESOURCE`. Otherwise file type is `GENERAL`. These types are important since they are used to define parsing class for robot API. """ files = self.config.paths file_type_checker = FileTypeChecker(self.config.exec_dir) for file in self.get_files(files, self.config.recursive): if '__init__' in file.name: file_type = FileType.INIT elif file.suffix.lower() == '.resource': file_type = FileType.RESOURCE else: file_type = FileType.GENERAL file_type_checker.source = file try: model = file_type.get_parser()(str(file)) file_type_checker.visit(model) self.files[file] = (file_type, model) except DataError: print( f"Failed to decode {file}. Default supported encoding by Robot Framework is UTF-8. Skipping file" ) for resource in file_type_checker.resource_files: if resource in self.files and self.files[resource][ 0].value != FileType.RESOURCE: self.files[resource] = (FileType.RESOURCE, get_resource_model(str(resource))) def run_checks(self): for file in self.files: if self.config.verbose: print(f"Scanning file: {file}") model = self.files[file][1] found_issues = self.run_check(model, str(file)) issues_to_lsp_diagnostic(found_issues) found_issues.sort() for issue in found_issues: self.report(issue) if 'file_stats' in self.reports: self.reports['file_stats'].files_count = len(self.files) def run_check(self, ast_model, filename, source=None): found_issues = [] self.register_disablers(filename, source) if self.disabler.file_disabled: return [] for checker in self.checkers: if checker.disabled: continue found_issues += [ issue for issue in checker.scan_file(ast_model, filename, source) if not self.disabler.is_rule_disabled(issue) ] return found_issues def register_disablers(self, filename, source): """ Parse content of file to find any disabler statements like # robocop: disable=rulename """ self.disabler = DisablersFinder(filename=filename, source=source) def report(self, rule_msg): for report in self.reports.values(): report.add_message(rule_msg) try: source_rel = os.path.relpath(os.path.expanduser(rule_msg.source), self.root) except ValueError: source_rel = rule_msg.source self.log_message(source=rule_msg.source, source_rel=source_rel, line=rule_msg.line, col=rule_msg.col, end_line=rule_msg.end_line, end_col=rule_msg.end_col, severity=rule_msg.severity.value, rule_id=rule_msg.rule_id, desc=rule_msg.desc, name=rule_msg.name) def log_message(self, **kwargs): self.write_line(self.config.format.format(**kwargs)) def load_checkers(self): self.checkers = [] self.rules = {} checkers.init(self) def list_checkers(self): if not (self.config.list or self.config.list_configurables): return if self.config.list_configurables: print( "All rules have configurable parameter 'severity'. Allowed values are:" "\n E / error\n W / warning\n I / info") rule_by_id = { msg.rule_id: (msg, checker) for checker in self.checkers for msg in checker.rules_map.values() } rule_ids = sorted([key for key in rule_by_id]) for rule_id in rule_ids: rule_def, checker = rule_by_id[rule_id] if self.config.list: if rule_def.matches_pattern(self.config.list): print(rule_def) else: if not rule_def.matches_pattern( self.config.list_configurables): continue configurables = rule_def.available_configurables( include_severity=False, checker=checker) if configurables: print(f"{rule_def}\n" f" {configurables}") sys.exit() def load_reports(self): self.reports = dict() classes = inspect.getmembers(reports, inspect.isclass) available_reports = 'Available reports:\n' for report_class in classes: if not issubclass(report_class[1], reports.Report): continue report = report_class[1]() if not hasattr(report, 'name'): continue if 'all' in self.config.reports or report.name in self.config.reports: self.reports[report.name] = report available_reports += f'{report.name:20} - {report.description}\n' if self.config.list_reports: available_reports += 'all' + ' ' * 18 + '- Turns on all available reports' print(available_reports) sys.exit() def register_checker(self, checker): if not self.any_rule_enabled(checker): checker.disabled = True for rule_name, rule in checker.rules_map.items(): if rule_name in self.rules: (_, checker_prev) = self.rules[rule_name] raise robocop.exceptions.DuplicatedRuleError( 'name', rule_name, checker, checker_prev) if rule.rule_id in self.rules: (_, checker_prev) = self.rules[rule.rule_id] raise robocop.exceptions.DuplicatedRuleError( 'id', rule.rule_id, checker, checker_prev) self.rules[rule_name] = (rule, checker) self.rules[rule.rule_id] = (rule, checker) self.checkers.append(checker) def make_reports(self): for report in self.reports.values(): output = report.get_report() if output is not None: self.write_line(output) def get_files(self, files_or_dirs, recursive): for file in files_or_dirs: yield from self.get_absolute_path(Path(file), recursive) def get_absolute_path(self, path, recursive): if not path.exists(): raise robocop.exceptions.FileError(path) if self.config.is_path_ignored(path): return if path.is_file(): if self.should_parse(path): yield path.absolute() elif path.is_dir(): for file in path.iterdir(): if file.is_dir() and not recursive: continue yield from self.get_absolute_path(file, recursive) def should_parse(self, file): """ Check if file extension is in list of supported file types (can be configured from cli) """ return file.suffix and file.suffix.lower() in self.config.filetypes def any_rule_enabled(self, checker): for name, rule in checker.rules_map.items(): rule.enabled = self.config.is_rule_enabled(rule) checker.rules_map[name] = rule return any(msg.enabled for msg in checker.rules_map.values()) def configure_checkers_or_reports(self): for config in self.config.configure: if config.count(':') < 2: raise robocop.exceptions.ConfigGeneralError( f"Provided invalid config: '{config}' (general pattern: <rule>:<param>:<value>)" ) rule_or_report, param, value, *values = config.split(':') if rule_or_report in self.rules: msg, checker = self.rules[rule_or_report] if param == 'severity': self.rules[rule_or_report] = (msg.change_severity(value), checker) else: configurable = msg.get_configurable(param) if configurable is None: available_conf = msg.available_configurables( checker=checker) raise robocop.exceptions.ConfigGeneralError( f"Provided param '{param}' for rule '{rule_or_report}' does not exist. " f"Available configurable(s) for this rule:\n" f" {available_conf}") checker.configure(configurable[1], configurable[2](value)) elif rule_or_report in self.reports: self.reports[rule_or_report].configure(param, value, *values) else: similiar = RecommendationFinder().find_similar( rule_or_report, self.rules) raise robocop.exceptions.ConfigGeneralError( f"Provided rule or report '{rule_or_report}' does not exist.{similiar}" )