Skip to content

gate-3 stub-scan: a run() that delegates to one helper is reported as a stub, and the gate closes on a dead line #226

Description

@rubenvdlinde

Measured on portaliq origin/development @ 79bbf68, full-tree, hydra-gates @ 756fe89.

[gate-3] stub-scan: FAIL — 1 finding(s)
lib/BackgroundJob/NotificationDispatchJob.php: run() body has no non-logger statements (stub)

That file is 530 lines with 11 private methods implementing a complete notification pipeline (account lookup → render → send → append-only log → fallback flag). It is not a stub by any reading.

The arithmetic

scripts/run-hydra-gates.sh:840-842:

_body=$(awk "/function run\(/,/^    }/" "${job}" \
  | grep -vE "^\s*(//|\*|\s*\{|\s*\}|\s*$)" \
  | grep -vE "function run|logger->(info|warning|debug|error|notice)|try\s*\{|\}\s*catch|return;?$")
_lc=$(echo "${_body}" | grep -cE "\S")
[ "${_lc}" -lt 2 ] && echo "... run() body has no non-logger statements (stub)"

The filters strip try {, } catch, and every logger call. For the canonical fail-safe wrapper —

protected function run($argument): void
{
    try {
        $this->doRun(argument: $argument);
    } catch (Throwable $e) {
        $this->logger->error("...", ["reason" => $e->getMessage()]);
    }
}

— exactly one line survives, and the threshold is < 2.

Fixtures

Running the gate's own pipeline over three run() bodies:

fixture surviving lines verdict
A — genuine stub (logger->info() only) 0 FLAGGED ✅ correct
B — real fail-safe delegation to doRun() 1 FLAGGED ❌ false positive
C — B plus one inert $unused = 1; 2 passes

C is the problem. The gate is closed by adding a dead line and cannot be closed by writing correct code. The only "fixes" available to a builder are:

  1. add a no-op statement (C) — satisfies the gate, changes nothing;
  2. inline the whole pipeline back into run() — deletes the single try/catch that keeps an exception from escaping to the NC cron runner.

Both make the code worse. This is the same family as #184/#191/#220/#224: a text-shaped heuristic standing in for a semantic property, except here the remediation is actively harmful rather than merely noisy.

Why the pattern exists

Wrapping one delegate in one try/catch is how a QueuedJob guarantees it never disrupts the cron runner — the job's docblock says exactly that. It is a good shape, and the gate penalises it specifically because the try/catch scaffolding and the logger call are both filtered out, leaving the single meaningful statement below threshold.

Suggested direction

A run() whose only statement is a call to a method defined in the same class is delegation, not a stub — resolve the callee and judge that body, or simply treat "one non-logger statement that is a method call" as implemented. The < 2 line count cannot distinguish "empty" from "one well-factored call", and padding it is a worse outcome than the finding.

No change was made to portaliq for this finding — it is being reported rather than "fixed", because every available fix is a regression.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions