"""Rules for dependency checking."""

# DepsInfo provides a list of dependencies found when building a target.
DepsInfo = provider(
    "lists dependencies encountered while building",
    fields = {
        "nodes": "a dict from targets to a list of their dependencies",
    },
)

def _deps_check_impl(target, ctx):
    # Check the target's dependencies and add any of our own deps.
    deps = []
    for dep in ctx.rule.attr.deps:
        deps.append(dep)
    nodes = {}
    if len(deps) != 0:
        nodes[target] = deps

    # Keep and propagate each dep's providers.
    for dep in ctx.rule.attr.deps:
        nodes.update(dep[DepsInfo].nodes)

    return [DepsInfo(nodes = nodes)]

_deps_check = aspect(
    implementation = _deps_check_impl,
    attr_aspects = ["deps"],
)

def _is_allowed(target, allowlist, prefixes):
    # Check for allowed prefixes.
    for prefix in prefixes:
        workspace, pfx = prefix.split("//", 1)
        if len(workspace) > 0 and workspace[0] == "@":
            workspace = workspace[1:]
        if target.workspace_name == workspace and target.package.startswith(pfx):
            return True

    # Check the allowlist.
    for allowed in allowlist:
        if target == allowed.label:
            return True

    return False

def _deps_test_impl(ctx):
    nodes = {}
    for target in ctx.attr.targets:
        for (node_target, node_deps) in target[DepsInfo].nodes.items():
            # Ignore any disallowed targets. This generates more useful error
            # messages. Consider the case where A dependes on B and B depends
            # on C, and both B and C are disallowed. Avoid emitting an error
            # that B depends on C, when the real issue is that A depends on B.
            if not _is_allowed(node_target.label, ctx.attr.allowed, ctx.attr.allowed_prefixes) and node_target.label != target.label:
                continue
            bad_deps = []
            for dep in node_deps:
                if not _is_allowed(dep.label, ctx.attr.allowed, ctx.attr.allowed_prefixes):
                    bad_deps.append(dep)
            if len(bad_deps) > 0:
                nodes[node_target] = bad_deps

    # If there aren't any violations, write a passing test.
    if len(nodes) == 0:
        ctx.actions.write(
            output = ctx.outputs.executable,
            content = "#!/bin/bash\n\nexit 0\n",
        )
        return []

    # If we're here, we've found at least one violation.
    script_lines = [
        "#!/bin/bash",
        "echo Invalid dependencies found. If you\\'re sure you want to add dependencies,",
        "echo modify this target.",
        "echo",
    ]

    # List the violations.
    for target, deps in nodes.items():
        script_lines.append(
            'echo "{target} depends on:"'.format(target = target.label),
        )
        for dep in deps:
            script_lines.append('echo "\t{dep}"'.format(dep = dep.label))

    # The test must fail.
    script_lines.append("exit 1\n")

    ctx.actions.write(
        output = ctx.outputs.executable,
        content = "\n".join(script_lines),
    )
    return []

# Checks that targets only depend on an allowlist of other targets. Targets can
# be specified directly, or prefixes can be used to allow entire packages or
# directory trees.
#
# This recursively checks the "deps" attribute of each target, dependencies
# expressed other ways are not checked. For example, protobuf targets pull in
# protobuf code, but aren't analyzed by deps_test.
deps_test = rule(
    implementation = _deps_test_impl,
    attrs = {
        "targets": attr.label_list(
            doc = "The targets to check the transitive dependencies of.",
            aspects = [_deps_check],
        ),
        "allowed": attr.label_list(
            doc = "The allowed dependency targets.",
        ),
        "allowed_prefixes": attr.string_list(
            doc = "Any packages beginning with these prefixes are allowed.",
        ),
    },
    test = True,
)