summaryrefslogtreecommitdiff
path: root/bin/gradle-report
blob: bbee2b7c6533e430c98c950a8516c689532fb19d (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
#!/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" ;;
    lint)   TASK=":app:lintDebug"; REPORT="$PROJECT_DIR/app/build/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
    echo "report  : MISSING ($REPORT)"
    echo "result  : $([ $STATUS -eq 0 ] && echo 'PASS (no report produced, nothing to read)' || echo 'FAIL')"
    rm -f "$LOG"
    exit $STATUS
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" ]