Skip to content

feat(apidocs): Lint request parameters read without a serializer - #123921

Merged
azulus merged 4 commits into
masterfrom
jeremy/input-rules-2-shaped-reads
Sep 9, 2026
Merged

feat(apidocs): Lint request parameters read without a serializer#123921
azulus merged 4 commits into
masterfrom
jeremy/input-rules-2-shaped-reads

Conversation

@azulus

@azulus azulus commented Sep 9, 2026

Copy link
Copy Markdown
Member

Reports a parameter read straight off request.GET or request.data instead of through a serializer.

Such a parameter is invisible twice over. The schema has nothing to generate from, so no client can send it, and the value arrives as an unchecked string, so a misspelled key fails in production rather than in review. Reading through a serializer named in @extend_schema fixes both at once, because drf-spectacular derives the documented parameters from the same field objects the handler validates with. Ten public endpoints already work this way; this rule reports everyone else.

Three diagnostics share the rule, separating cases that differ in how they get fixed: a literal key read straight off the input, a key computed at runtime that no schema could ever document, and the whole dict handed to a callable so that what it reads is unknowable from the call site. Counting or iterating the dict is not a hand-off, building a serializer from it is the target rather than a violation, and reads through validated_data are accepted.

A read has to hang off request or self.request to count. serializer.data and response.data are outputs a handler builds, not parameters a client sent, and subscript writes such as request.data["title"] = default are not reads either.

The rule is absent from ENFORCED, so nothing gates. The backlog is 197 raw reads, two computed keys, and 17 hand-offs.

Follows #123919, which added the ratchet this rule hangs off.

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Sep 9, 2026
@azulus
azulus force-pushed the jeremy/input-rules-1-ratchet-and-declared branch from 54d6ca8 to 94f77b8 Compare September 9, 2026 17:17
@azulus
azulus force-pushed the jeremy/input-rules-2-shaped-reads branch from 4e181df to bc0f02d Compare September 9, 2026 17:18
@azulus azulus changed the title feat(lint): Report endpoint input read outside a declared shape feat(apidocs): Lint request parameters read without a serializer Sep 9, 2026
@azulus
azulus marked this pull request as ready for review September 9, 2026 17:26
@azulus
azulus requested review from a team as code owners September 9, 2026 17:26
Comment thread tools/flake8_plugin.py
@azulus
azulus force-pushed the jeremy/input-rules-1-ratchet-and-declared branch from 5d8f486 to 7bb98e5 Compare September 9, 2026 17:56
@azulus
azulus force-pushed the jeremy/input-rules-2-shaped-reads branch from bc0f02d to 8da245c Compare September 9, 2026 17:56

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8da245c. Configure here.

Comment thread tools/flake8_plugin.py
Comment thread tools/flake8_plugin.py
Comment thread tools/flake8_plugin.py
Base automatically changed from jeremy/input-rules-1-ratchet-and-declared to master September 9, 2026 18:49
azulus and others added 2 commits September 9, 2026 11:50
A parameter read straight off request.GET or request.data is invisible twice.
The schema has nothing to generate from, so no client can send it, and the value
is an unchecked string, so a misspelled key fails at runtime rather than in
review. Reading through a serializer named in @extend_schema fixes both at once,
because drf-spectacular derives the documented parameters from the same fields
the handler validates with.

Adds three diagnostics under one rule:

  S026  a literal key read straight off the query string or request body
  S027  a key computed at runtime, which no schema can document
  S028  the whole dict handed to a callable, so what is read is unknowable

Counting or iterating the dict is not a hand-off, and building a serializer from
it is the target rather than a violation, so neither is reported. Reads through
validated_data are accepted.

The rule is absent from ENFORCED, so nothing gates. The backlog is 208 raw reads
across 67 files, 2 computed keys and 21 hand-offs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both review bots caught the same false positive. `_is` matched any attribute
named `data`, so `serializer.data["title"]` and `response.data["title"]` were
reported as raw request-body reads. Those are outputs a handler builds, not
parameters a client sent, and reporting them inflates the backlog and would
fail CI once the rule is enforced. The attribute now has to hang off `request`
or `self.request`.

`visit_Subscript` also recorded every subscript regardless of context, so
`request.data["title"] = default` counted as a read of a parameter that no
client supplies. It now records loads only.

Together these drop the reported backlog from 208 raw reads to 197 and from 21
hand-offs to 12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@azulus
azulus force-pushed the jeremy/input-rules-2-shaped-reads branch from 8da245c to 4dc8b45 Compare September 9, 2026 18:52
Comment thread tools/flake8_plugin.py Outdated
The guard that keeps a serializer construction from counting as a hand-off
matched any call with a data= keyword, so `my_func(data=request.GET)` was
silently dropped while the same call written positionally reported S028. The
keyword name is not what makes a call safe; the callee being a serializer is.

Both paths now share one predicate for what looks like a class, so they cannot
disagree again. S025 already used that test to skip plain calls and
runtime-chosen classes, and the hand-off guard skipped nothing at all.

Surfaces 5 hand-offs that were dropped by both rules at once: three
`serializer_cls(data=...)` sites where the class is chosen at runtime, and the
integration issue-config calls that take the whole body. Those are exactly the
unanalyzable cases S028 exists to report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread tools/flake8_plugin.py
A read accessor on something that is not the request fell through to hand-off
detection, so `options.get("key", request.GET)` reported that the query string
was handed to `get`. The dict is a default value there, not something a callee
reads parameters out of, and naming `get` as the callee made the diagnostic
read as nonsense.

A read method now returns once handled: on the request it is the read itself,
and on anything else its arguments are ordinary lookup arguments.

No occurrences on the current tree, so the counts are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@azulus
azulus merged commit 399ec82 into master Sep 9, 2026
69 checks passed
@azulus
azulus deleted the jeremy/input-rules-2-shaped-reads branch September 9, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants