Skip to content

build: a declared return type is checked rather than trusted, so an unchecked Any cannot reach a caller - #330

Merged
FBumann merged 1 commit into
fix/pyrefly-gatefrom
fix/checked-returns
Sep 1, 2026
Merged

build: a declared return type is checked rather than trusted, so an unchecked Any cannot reach a caller#330
FBumann merged 1 commit into
fix/pyrefly-gatefrom
fix/checked-returns

Conversation

@FBumann

@FBumann FBumann commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Prompt: "Any new rules we should add?" — answered with three, taken with "Do it. Stacked".

Note

The following content was generated by AI.

Forcing all ~120 of pyrefly's error kinds on the #325 tree leaves exactly seven ungated, and only three carry signal rather than noise. Those three land here — no-any-return-explicit, no-any-return-implicit, unknown-variable-type — with the eight sites they name, plus strict-callable-subtyping = true.

The rule that matters is no-any-return-*: it catches a function whose declared return type the checker never actually verified, which is a wrong value reaching a caller under a type that promised otherwise.

site was now
model.py:712 _expansion: Any `_ExpandedSpec
errors.py:91 schema_error(exc: Any) exc: ValidationError — what the docstring already said it takes
errors.py:104 str(error.get('msg', '')) the typed exc made this redundant, and #325's own rule caught it
model.py:562 return handler(v) cast('PiecewiseMethod', …) — pydantic's wrap handler returns Any; the cast is where that stops
model.py:777 return _without_absence(handler(self)) cast('dict[str, Any]', …)
_yaml.py:73 untyped loop over node.value pairs: list[tuple[yaml.Node, yaml.Node]]
_where_parser.py:179 result = items[0] result: WhereNode | UnresolvedWhereNode = items[0], and one cast('WhereNode', …) where the fold hands a parsed leaf to AndNode/OrNode — since #341 those fields are resolved nodes only
exclusivity.py:367 the one suppression; see below

Typing exc left Any unused in errors.py, and ruff removed the import.

Verified

pixi run ci exit 0: lint clean, 1003 passed, mkdocs build --strict builds, 27 TeX documents compile. pyrefly check is 0 errors, 9 suppressed — one of the nine being the one added here.

Mutation table — each fix reverted on the committed tree, restored with git checkout --
Mutation pyrefly check
committed tree exit 0 — 0 errors
_expansion back to Any exit 1 — 1 error
schema_error's exc back to Any exit 1 — 1 error
the PiecewiseMethod cast dropped exit 1 — 1 error
the WhereNode annotation dropped exit 1 — 1 error
strict-callable-subtyping off exit 0 — 0 errors
restored exit 0 — 0 errors

strict-callable-subtyping changes nothing on this tree in either direction, so the mutation proves nothing about it. A positive control does — a narrow function assigned where a *args: Any, **kwargs: Any protocol is expected, appended to degree.py and then reverted:

strict-callable-subtyping=true   exit=1  `(x: int) -> int` is not assignable to `_Loose` [bad-assignment]
strict-callable-subtyping=false  exit=0  0 errors

That is the shape set_parse_action takes, which is why the knob is here rather than deferred.

The one suppression

exclusivity.py:367 returns an implicit Any from _between(...) -> Any | None. Every fix available is worse than the line: declaring -> Any silences it by deleting the signature's statement that the discrete branch may have nothing to return, and a precise signature needs overloads across float/int/date for one private helper with one caller. Suppressed per line with that reason, which is what [tool.pyrefly]'s own comment asks for.

Declined, and why

implicit-bool (103 sites). Banning if some_list: for if len(some_list) > 0: fights Python idiom for no defect. But 27 of the 103 are | None types, where if x: conflates None with empty — sometimes intended, sometimes a bug. That is worth one manual sweep, not a rule; it is not in this PR and is not filed as an issue yet.

explicit-any (73), unknown-argument-type (44), unused-call-result (18). The pydantic doors and the pyparsing seam — the same diagnostics #325 argued against under basedpyright's names.

spec-compliant-overloads is not a strictness knob: its own help says it falls back to Any more often. strict-partial-subtyping is free at 0 errors but nothing in src/ imports functools.partial, so it gates nothing — left out under YAGNI.

Stack: on #325 (fix/pyrefly-gate), which this needs for the severity spelling to mean anything.

@read-the-docs-community

read-the-docs-community Bot commented Sep 1, 2026

Copy link
Copy Markdown

stack merge was automatically disabled September 1, 2026 15:52

Pull Request is not mergeable

@FBumann
FBumann force-pushed the fix/checked-returns branch from cd0c876 to 64a0ecb Compare September 1, 2026 16:00
stack merge was automatically disabled September 1, 2026 16:32

Pull Request is not mergeable

…nchecked Any cannot reach a caller

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FBumann
FBumann force-pushed the fix/checked-returns branch from 64a0ecb to 87929f0 Compare September 1, 2026 16:36
@FBumann
FBumann merged commit 0b8bd03 into main Sep 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant