Skip to content

cants exits 0 on FATAL and on a nonexistent input path, writing an empty-application analysis.json #181

Description

@rahlk

Problem

cants exits 0 when it fails. Two cases, both measured on 1.3.0:

$ cants -i /nonexistent/xyz -o out ; echo "rc=$?"
rc=0
$ cat out/analysis.json     # written anyway
{"schema_version":"2.0.0","language":"typescript","max_level":1,
 "analyzer":{"name":"codeanalyzer-typescript","version":"1.3.0"},
 "application":{"id":"can://typescript/xyz","kind":"application","symbol_table":{}, ...}}
$ cants -i vscode -o out ; echo "rc=$?"
[codeanalyzer-ts] FATAL TypeError: Type error
    at structuredClone (unknown)
    at finalizeAnalysis (...)
rc=0

So the analyzer returns success after printing FATAL, and returns success for an input path that does not exist — emitting a schema-valid payload describing an empty application, whose id is derived from the last path segment of a directory that was never read.

Why it matters

Every consumer runs this as a subprocess and the exit code is the only cheap health signal. A wrapper written the obvious way — subprocess.run(..., check=True) — treats both cases as a successful run. The nonexistent-path case is worse than the crash, because it hands back a well-formed payload: an agent asking "what does this project contain?" is told, in schema-conformant JSON, that the answer is nothing.

The FATAL case is at least distinguishable by the absent output directory, and the empty-input case by symbol_table == {} — but both require the consumer to know to look, and neither is what an exit code is for.

Expected

  • A nonexistent or unreadable -i path is an error: non-zero exit, no analysis.json written.
  • Any path that prints FATAL exits non-zero.

An empty symbol_table should mean "this project has no analysable sources", which is a real and different answer from "the input did not exist".

Environment

codeanalyzer-typescript 1.3.0 (PyPI wheel, bundled Bun binary), macOS 15 arm64.

Related: the crash in the second case is #180. This issue is about the exit code and the payload, which are independent of what went wrong.

Activity

  1. rahlk commented on Sep 7, 2026

    @rahlk
    ContributorAuthor

    Refinement: the exit code is inconsistent, not uniformly zero — so some failure paths already set it and these two do not.

    failure stderr exit
    nonexistent -i path (silent) 0
    structuredClone FATAL in finalizeAnalysis (#180) [codeanalyzer-ts] FATAL TypeError 0
    Neo4j rejects a commit during --emit neo4j [codeanalyzer-ts] FATAL Neo4jError 1

    All three print FATAL (except the first, which prints nothing at all), so the distinction is not "fatal vs recoverable" — it looks like the emit path propagates its failure to the process exit and the analysis path does not.

    That makes the fix narrower than I first suggested: the FATAL reporter itself should set a non-zero exit, rather than each call site remembering to. The nonexistent-path case is separate and needs its own guard, since it prints nothing and still writes a payload.

  2. rahlk commented on Sep 7, 2026

    @rahlk
    ContributorAuthor

    Closed by #187 for the input-path half (nonexistent or non-directory -i → one-line usage error, exit 1, nothing written; an empty real directory still exits 0 with an empty symbol_table).

    The FATAL → exit 0 half could not be reproduced on main: src/index.ts already exits 1 after printing FATAL, and the Neo4j case exits 1 through the same catch. A Bun 1.3.14 spike shows a whole-envelope structuredClone can abort the process outright (rc 133, no exception) rather than throw, so the 1.3.0 rc=0 after FATAL TypeError is unexplained. #188 removes that clone (per-module now), which removes the trigger. If a FATAL ever again exits 0, reopen with the exact stderr and rc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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