summaryrefslogtreecommitdiff
path: root/bin/gradle-report
blob: cc0de0382955c6902b861075b3c6230dd8c19ab4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env bash
# gradle-report <project-dir> <detekt|lint> [--allow-stale] [-- <gradle args>]
#
# Runs a static-analysis task and reports from its XML report, refusing to speak if the report
# is older than the run.
#
# Exists because detekt has a failure mode that looks exactly like findings: an invalid config
# property makes it abort BEFORE analysing anything, leaving the previous run's XML on disk.
# Three times in one session that stale file was read as the current result, and the line
# numbers never moved because the analysis never happened. The only honest signal was the exit
# code, which said "failed" while the report said "here are your 8 findings".
set -uo pipefail

die() { echo "gradle-report: $1" >&2; exit 2; }

[ $# -ge 2 ] || die "usage: gradle-report <project-dir> <detekt|lint> [-- <gradle args>]"

PROJECT_DIR=$(cd "$1" 2>/dev/null && pwd) || die "no such project directory: $1"
KIND="$2"
shift 2
ALLOW_STALE=0
if [ "${1:-}" = "--allow-stale" ]; then ALLOW_STALE=1; shift; fi
[ "${1:-}" = "--" ] && shift
EXTRA=("$@")

case "$KIND" in
    detekt) TASK=":app:detekt"; REPORT="$PROJECT_DIR/app/build/reports/detekt/detekt.xml" ;;
    # AGP has moved this file between versions, so locate it rather than assuming. A hardcoded
    # path that stops resolving turns into "MISSING ... PASS", which is worse than no check.
    lint)   TASK=":app:lintDebug"
            REPORT=$(find "$PROJECT_DIR/app/build" -name "lint-results-debug.xml" 2>/dev/null | head -1)
            REPORT=${REPORT:-$PROJECT_DIR/app/build/reports/lint-results-debug.xml} ;;
    *)      die "kind must be 'detekt' or 'lint', got '$KIND'" ;;
esac

[ -x "$PROJECT_DIR/gradlew" ] || die "no gradlew in $PROJECT_DIR"

# Marker predates the run, so any report not newer than it is stale.
MARKER=$(mktemp)
trap 'rm -f "$MARKER"' EXIT
sleep 1

LOG=$(mktemp)
( cd "$PROJECT_DIR" && ./gradlew "$TASK" "${EXTRA[@]}" ) >"$LOG" 2>&1
STATUS=$?

echo "=== gradle-report ($KIND) ==="
echo "project : $PROJECT_DIR"
echo "task    : $TASK"
echo "exit    : $STATUS"

# An invalid config aborts detekt before it analyses anything. Name it, because the report on
# disk will be the previous run's and will look perfectly reasonable.
if grep -qiE "is misspelled or does not exist|configuration file is invalid|invalid config" "$LOG"; then
    echo
    echo "CONFIG INVALID — analysis never ran. Offending properties:"
    grep -iE "is misspelled or does not exist|configuration file is invalid|invalid config" "$LOG" | sed 's/^/  /' | sort -u
    echo
    echo "result  : FAIL (config rejected; any report on disk is from an earlier run)"
    rm -f "$LOG"
    exit 1
fi

if [ ! -f "$REPORT" ]; then
    # Falling back to the exit code here is how a broken path becomes a silent pass. Say that
    # the check did not actually happen.
    echo "report  : MISSING ($REPORT)"
    echo "hint    : the report path may have moved; this check did NOT inspect any findings"
    echo "result  : FAIL — no report to read (gradle exit $STATUS)"
    rm -f "$LOG"
    exit 1
fi

if [ "$REPORT" -ot "$MARKER" ] && [ $ALLOW_STALE -eq 0 ]; then
    echo "report  : STALE — $REPORT predates this run"
    if grep -qE "Task :app:(detekt|lintDebug) UP-TO-DATE" "$LOG"; then
        echo "hint    : Gradle treated $TASK as UP-TO-DATE, so nothing was re-analysed and the"
        echo "          report on disk describes an earlier state of the code."
        echo "          Re-run with: -- --rerun-tasks   (or --allow-stale to accept it)"
    else
        echo "hint    : the task ran but produced no new report. Re-run with: -- --rerun-tasks"
    fi
    echo "result  : FAIL — refusing to report numbers that describe older code"
    rm -f "$LOG"
    exit 1
fi

FINDINGS=$(python3 - "$REPORT" "$KIND" <<'PY'
import sys, xml.etree.ElementTree as ET
path, kind = sys.argv[1], sys.argv[2]
try:
    root = ET.parse(path).getroot()
except Exception as e:
    print(f"UNREADABLE {e}")
    raise SystemExit
rows = []
if kind == "detekt":
    for f in root.iter("file"):
        for err in f.iter("error"):
            rows.append(f"{f.get('name','').split('/')[-1]}:{err.get('line')} "
                        f"[{(err.get('source') or '').split('.')[-1]}] {(err.get('message') or '')[:90]}")
else:
    for issue in root.iter("issue"):
        if issue.get("severity", "").lower() not in ("error", "fatal"):
            continue
        loc = next(issue.iter("location"), None)
        where = f"{(loc.get('file','') if loc is not None else '').split('/')[-1]}:{loc.get('line') if loc is not None else '?'}"
        rows.append(f"{where} [{issue.get('id')}] {(issue.get('message') or '')[:90]}")
print(f"COUNT {len(rows)}")
for r in rows:
    print(f"  {r}")
PY
)

if [ "$REPORT" -ot "$MARKER" ]; then
    echo "report  : STALE (accepted via --allow-stale)"
else
    echo "report  : fresh"
fi
echo "$FINDINGS" | head -1 | sed 's/^COUNT /findings: /'
echo "$FINDINGS" | tail -n +2

COUNT=$(echo "$FINDINGS" | head -1 | awk '{print $2}')
if [ "$COUNT" = "0" ] && [ $STATUS -eq 0 ]; then
    echo "result  : PASS (0 findings, gradle exit 0)"
else
    echo "result  : FAIL (${COUNT:-?} findings, gradle exit $STATUS)"
fi
rm -f "$LOG"
[ $STATUS -eq 0 ] && [ "$COUNT" = "0" ]