#!/bin/sh
# script/lint-audit: audit a captured golangci-lint run for output that
# cannot describe this tree. Called by script/lint on every run; usable
# on its own against any saved lint output.
#
#   script/lint-audit <capture-file>
#
# Exits 0 when every finding cites a file in this tree, 1 when any does
# not. It NEVER certifies that a lint run passed - it has no idea
# whether the run found issues, and does not look. It only rejects
# output that is impossible for this tree, which is a different and much
# weaker claim. Do not use it as a gate; use script/lint.
#
# Why this exists (issue #99): golangci-lint caches analysis results,
# and a cache shared between two checkouts of this repo can serve one
# checkout's stored findings for another, file paths included. The
# failure is symmetric and only one direction is loud - a clean tree
# failed by a dirty sibling gets investigated, while a dirty tree passed
# by a clean sibling is silent. This turns the silent direction into a
# hard error, which is why it runs on clean output too.
#
# The primary fix is that script/lint now keys its cache on the worktree
# path so the collision cannot happen. This is the backstop, because a
# backstop that only runs when we already believe things are fine is
# worth more than one more assumption.
set -eu

ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"

# Where script/lint bind-mounts the tree inside the pinned image. A
# containerized run that prints absolute paths (`--path-mode abs`)
# prints them under this, so they are this tree's files under another
# name. Note the consequence, and why the cache key rather than this
# check is the real fix: two containerized runs of different checkouts
# both call themselves /src, so contamination between two container
# runs is not distinguishable by path alone.
CONTAINER_ROOT="/src"

usage() {
    echo "usage: $(basename "$0") <capture-file>" >&2
    exit 2
}

# Every path cited by a finding that is not a file in this tree.
#
# The linter runs with the tree root as its working directory, so a
# legitimate finding cites either a relative path that resolves inside
# the tree or an absolute path under the root. A path that escapes
# (absolute and elsewhere, or with a `..` component) or that names a
# file which is not here describes something this run did not analyse.
foreign_paths() {
    capture="$1"
    awk -F: '$1 ~ /\.go$/ && $2 ~ /^[0-9]+$/ { print $1 }' "$capture" |
        sort -u |
        while IFS= read -r path; do
            case "$path" in
                "$ROOT"/*)
                    path="${path#"$ROOT"/}"
                    ;;
                "$CONTAINER_ROOT"/*)
                    path="${path#"$CONTAINER_ROOT"/}"
                    ;;
                /*)
                    printf '%s\n' "$path"
                    continue
                    ;;
                ../* | */../*)
                    printf '%s\n' "$path"
                    continue
                    ;;
            esac
            if [ ! -e "$ROOT/$path" ]; then
                printf '%s\n' "$path"
            fi
        done
}

main() {
    [ "$#" -eq 1 ] || usage
    capture="$1"
    if [ ! -f "$capture" ]; then
        echo "lint-audit: no such capture file: $capture" >&2
        exit 2
    fi

    foreign="$(foreign_paths "$capture")"
    if [ -z "$foreign" ]; then
        exit 0
    fi

    cat >&2 <<EOF

lint: REJECTED - the linter reported findings for files that are not in
this tree, so its output does not describe the tree that was linted.
This result is void, whichever way it went: a pass here would be a pass
earned by analysing someone else's code.

  tree: $ROOT

Paths reported that are not in this tree:
EOF
    printf '%s\n' "$foreign" | sed -e 's/^/  /' >&2
    cat >&2 <<EOF

This is the signature of analysis replayed from a cache belonging to
another checkout (issue #99). Clear this tree's lint cache and re-run:

  rm -rf "\${XDG_CACHE_HOME:-\$HOME/.cache}/vaultik-lint"
EOF
    exit 1
}

main "$@"
