Exemplo n.º 1
0
def _calculate_head_comparison(
        target_file_manager: TargetFileManager,
        tools: Iterable[Tool]) -> Tuple[Baseline, float]:
    """
    Calculates a baseline consisting of all findings from the branch head

    If no HEAD branch exists return empty baseline

    :param paths: Which paths are being checked
    :param tools: Which tools to check
    :return: The branch head baseline
    """
    try:
        with target_file_manager.run_context(True,
                                             RunStep.BASELINE) as target_paths:
            runner = Runner(paths=target_paths,
                            use_cache=True,
                            skip_setup=True)
            if len(runner.paths) > 0:
                before = time.time()
                comparison_results = runner.parallel_results(tools, {},
                                                             keep_bars=False)
                baseline = {
                    tool_id: {f.syntactic_identifier_str()
                              for f in findings}
                    for tool_id, findings in comparison_results
                    if isinstance(findings, list)
                }
                elapsed = time.time() - before
                return baseline, elapsed
            else:
                return {}, 0.0
    except NoGitHeadException:
        logging.debug("No git head found so defaulting to empty head baseline")
        return {}, 0.0
Exemplo n.º 2
0
def orchestrate(
    baseline: Baseline,
    target_file_manager: TargetFileManager,
    staged: bool,
    tools: Iterable[Tool],
) -> Tuple[Collection[RunResults], float]:
    """
        Manages interactions between TargetFileManager, Runner and Tools

        Uses passed target_file_manager, staged flag, and tool list to setup
        Runner and runs tools on relevant files, returning aggregated output
        of tool running and time to run all tools in parallel
    """
    elapsed = 0.0
    if staged:
        head_baseline, elapsed = _calculate_head_comparison(
            target_file_manager, tools)
        for t in tools:
            tool_id = t.tool_id()
            if tool_id not in baseline:
                baseline[tool_id] = head_baseline.get(tool_id, set())
            else:
                baseline[tool_id].update(head_baseline.get(tool_id, set()))

    with target_file_manager.run_context(staged,
                                         RunStep.CHECK) as target_paths:
        use_cache = not staged  # if --all then can use cache
        skip_setup = staged  # if check --all then include setup
        runner = Runner(paths=target_paths,
                        use_cache=use_cache,
                        skip_setup=skip_setup)

        if len(runner.paths) == 0:
            echo_warning(
                f"Nothing to check or archive. Please confirm that changes are staged and not excluded by `{IGNORE_FILE_NAME}`. To check all Git tracked files, use `--all`."
            )
            click.secho("", err=True)
            all_results: Collection[RunResults] = []
            elapsed = 0.0
        else:
            before = time.time()
            all_results = runner.parallel_results(tools, baseline)
            elapsed += time.time() - before

    return all_results, elapsed
Exemplo n.º 3
0
    def _install_config_if_not_exists(self) -> bool:
        """
        Installs .bento.yml if one does not already exist

        :return: whether a config was installed
        """
        config_path = self.context.config_path
        pretty_path = self.context.pretty_path(config_path)
        if not config_path.exists():
            on_done = content.InstallConfig.install.echo(pretty_path)
            with (open(
                    os.path.join(os.path.dirname(__file__),
                                 "../configs/default.yml"))) as template:
                yml = yaml.safe_load(template)

            target_file_manager = TargetFileManager(
                self.context.base_path,
                [self.context.base_path],
                staged=False,
                ignore_rules_file_path=self.context.ignore_file_path,
            )

            for tid, tool in self.context.tool_inventory.items():
                if (not tool(self.context).matches_project(
                        target_file_manager._target_paths)
                        and tid in yml["tools"]):
                    del yml["tools"][tid]
            logging.debug(
                f"Matching tools for this project: {', '.join(yml['tools'].keys())}"
            )
            if not yml["tools"]:
                logging.warning("No tools match this project")
            with config_path.open("w") as config_file:
                yaml.safe_dump(yml, stream=config_file)
            on_done()
            logging.info(f"Created {pretty_path}.")
            return True
        else:
            content.InstallConfig.install.echo(pretty_path, skip=True)
            return False
Exemplo n.º 4
0
def archive(context: Context, all_: bool, paths: Tuple[Path, ...]) -> None:
    """
    Suppress current findings.

    By default, only results introduced by currently staged changes will be
    added to the archive (`.bento/archive.json`). Archived findings will
    not appear in future `bento check` output and will not block commits if
    `autorun` is enabled.

    Use `--all` to archive findings in all Git tracked files, not just those
    that are staged:

        $ bento archive --all [PATHS]

    Optional PATHS can be specified to archive results from specific directories
    or files.

    Archived findings are viewable in `.bento/archive.json`.
    """
    # Default to no path filter
    if len(paths) < 1:
        path_list = [context.base_path]
    else:
        path_list = list(paths)

    if not context.is_init:
        if all_:
            click.echo(f"Running Bento archive on all tracked files...\n", err=True)
        else:
            click.echo(f"Running Bento archive on staged files...\n", err=True)

    if not context.config_path.exists():
        echo_error("No Bento configuration found. Please run `bento init`.")
        sys.exit(3)

    if context.baseline_file_path.exists():
        with context.baseline_file_path.open() as json_file:
            old_baseline = bento.result.load_baseline(json_file)
            old_hashes = {
                h
                for findings in old_baseline.values()
                for h in findings.get(VIOLATIONS_KEY, {}).keys()
            }
    else:
        old_baseline = {}
        old_hashes = set()

    new_baseline: Dict[str, Dict[str, Dict[str, Any]]] = {}
    tools = context.tools.values()

    target_file_manager = TargetFileManager(
        context.base_path, path_list, not all_, context.ignore_file_path
    )
    target_paths = target_file_manager.get_target_files()
    all_findings, elapsed = bento.orchestrator.orchestrate(
        context, target_paths, not all_, tools
    )

    n_found = 0
    n_existing = 0
    found_hashes: Set[str] = set()

    for tool_id, vv in all_findings:
        if isinstance(vv, Exception):
            raise vv
        # Remove filtered
        vv = [f for f in vv if not f.filtered]
        n_found += len(vv)
        new_baseline[tool_id] = bento.result.dump_results(vv)
        if tool_id in old_baseline:
            new_baseline[tool_id][VIOLATIONS_KEY].update(
                old_baseline[tool_id][VIOLATIONS_KEY]
            )
        for v in vv:
            h = v.syntactic_identifier_str()
            found_hashes.add(h)
            if h in old_hashes:
                n_existing += 1

    n_new = n_found - n_existing

    context.baseline_file_path.parent.mkdir(exist_ok=True, parents=True)
    with context.baseline_file_path.open("w") as json_file:
        bento.result.write_tool_results(json_file, new_baseline)

    finding_source_text = "in this project" if all_ else "due to staged changes"
    success_str = f"{n_new} finding(s) {finding_source_text} were archived, and will be hidden in future Bento runs."
    if n_existing > 0:
        success_str += f"\nBento also kept {n_existing} existing finding(s)."

    click.echo(success_str, err=True)

    if not context.is_init:
        echo_newline()
        echo_next_step("To view archived results", "cat .bento/archive.json")
Exemplo n.º 5
0
def check(
    context: Context,
    all_: bool = False,
    formatter: Tuple[str, ...] = (),
    pager: bool = True,
    tool: Optional[str] = None,
    staged_only: bool = False,  # Should not be used. Legacy support for old pre-commit hooks
    paths: Tuple[Path, ...] = (),
) -> None:
    """
    Checks for new findings.

    By default, only staged files are checked. New findings introduced by
    these staged changes AND that are not in the archive (`.bento/archive.json`)
    will be shown.

    Use `--all` to check all Git tracked files, not just those that are staged:

        $ bento check --all [PATHS]

    Optional PATHS can be specified to check specific directories or files.

    See `bento archive --help` to learn about suppressing findings.
    """

    # Fail out if not configured
    if not context.config_path.exists():
        raise NoConfigurationException()

    # Fail out if no .bentoignore
    if not context.ignore_file_path.exists():
        raise NoIgnoreFileException(context)

    # Default to no path filter
    if len(paths) < 1:
        path_list = [context.base_path]
    else:
        path_list = list(paths)

    # Handle specified tool that is not configured
    if tool and tool not in context.configured_tools:
        click.echo(
            f"{tool} has not been configured. Adding default configuration for tool to {bento.constants.CONFIG_FILE_NAME}"
        )
        update_tool_run(context, tool, False)
        # Set configured_tools to None so that future calls will
        # update and include newly added tool
        context._configured_tools = None

    # Handle specified formatters
    if formatter:
        context.config["formatter"] = [{f: {}} for f in formatter]

    if all_:
        click.echo(f"Running Bento checks on all tracked files...\n", err=True)
    else:
        click.echo(f"Running Bento checks on staged files...\n", err=True)

    tools: Iterable[Tool[Any]] = context.tools.values()
    if tool:
        tools = [context.configured_tools[tool]]

    baseline: Baseline = {}
    if context.baseline_file_path.exists():
        with context.baseline_file_path.open() as json_file:
            baseline = bento.result.json_to_violation_hashes(json_file)

    target_file_manager = TargetFileManager(
        context.base_path, path_list, not all_, context.ignore_file_path
    )

    all_results, elapsed = bento.orchestrator.orchestrate(
        baseline, target_file_manager, not all_, tools
    )

    fmts = context.formatters
    findings_to_log: List[Any] = []
    n_all = 0
    n_all_filtered = 0
    filtered_findings: Dict[str, List[Violation]] = {}
    for tool_id, findings in all_results:
        if isinstance(findings, Exception):
            logging.error(findings)
            echo_error(f"Error while running {tool_id}: {findings}")
            if isinstance(findings, BentoException):
                click.secho(findings.msg, err=True)
            else:
                if isinstance(findings, subprocess.CalledProcessError):
                    click.secho(findings.stderr, err=True)
                    click.secho(findings.stdout, err=True)
                if isinstance(findings, NodeError):
                    echo_warning(
                        f"Node.js not found or version is not compatible with ESLint v6."
                    )
                click.secho(
                    f"""-------------------------------------------------------------------------------------------------
    This may be due to a corrupted tool installation. You might be able to fix this issue by running:

    bento init --clean

    You can also view full details of this error in `{bento.constants.DEFAULT_LOG_PATH}`.
    -------------------------------------------------------------------------------------------------
    """,
                    err=True,
                )
            context.error_on_exit(ToolRunException())
        elif isinstance(findings, list) and findings:
            findings_to_log += bento.metrics.violations_to_metrics(
                tool_id,
                context.timestamp,
                findings,
                __get_ignores_for_tool(tool_id, context.config),
            )
            filtered = [f for f in findings if not f.filtered]
            filtered_findings[tool_id] = filtered

            n_all += len(findings)
            n_filtered = len(filtered)
            n_all_filtered += n_filtered
            logging.debug(f"{tool_id}: {n_filtered} findings passed filter")

    def post_metrics() -> None:
        bento.network.post_metrics(findings_to_log, is_finding=True)

    stats_thread = threading.Thread(name="stats", target=post_metrics)
    stats_thread.start()

    dumped = [f.dump(filtered_findings) for f in fmts]
    context.start_user_timer()
    bento.util.less(dumped, pager=pager, overrun_pages=OVERRUN_PAGES)
    context.stop_user_timer()

    finding_source_text = "in this project" if all_ else "due to staged changes"
    if n_all_filtered > 0:
        echo_warning(
            f"{n_all_filtered} finding(s) {finding_source_text} in {elapsed:.2f} s"
        )
        click.secho("\nPlease fix these issues, or:\n", err=True)
        echo_next_step("To archive findings as tech debt", f"bento archive")
        echo_next_step("To disable a specific check", f"bento disable check TOOL CHECK")
    else:
        echo_success(f"0 findings {finding_source_text} in {elapsed:.2f} s\n")

    n_archived = n_all - n_all_filtered
    if n_archived > 0:
        echo_next_step(
            f"Not showing {n_archived} archived finding(s). To view",
            "cat .bento/archive.json",
        )

    if not all_ and not context.autorun_is_blocking:
        return
    elif context.on_exit_exception:
        raise context.on_exit_exception
    elif n_all_filtered > 0:
        sys.exit(2)