Skip to content

Add NoParamsInWorkPackageWhereId cop#4

Open
akabiru wants to merge 3 commits intomainfrom
feature/no-params-in-work-package-where-id
Open

Add NoParamsInWorkPackageWhereId cop#4
akabiru wants to merge 3 commits intomainfrom
feature/no-params-in-work-package-where-id

Conversation

@akabiru
Copy link
Copy Markdown
Member

@akabiru akabiru commented May 4, 2026

Catches WorkPackage.where(id: params[...]) patterns that silently drop semantic work package identifiers (e.g. "PROJ-42") because PostgreSQL casts the string to integer 0 inside where(id: ...) and the row set comes back empty. The cop suggests WorkPackage.where_display_id_in(...) (added in opf/openproject#23016), which partitions numeric and semantic inputs and consults the alias table.

Background

Self-review thread on opf/openproject#23016 raised the question of whether to extend the runtime guard already in place on WorkPackage.find_by(id: "PROJ-42") (which raises UnsupportedLookup) to WorkPackage.where(id: "PROJ-42"). The conclusion was: don't — where is AR's universal predicate primitive, composed into joins, scopes, association preloading, and Rails internals; guarding it would invert the docstring's "low-level code uses primary keys" rationale and risks subtle breakage. The actual bug class is narrow (user input flowing into where(id:)) and lints cleanly. See discussion.

Behaviour

The cop fires only when:

  • the receiver chain demonstrably resolves to a WorkPackage relation — either rooted at the WorkPackage constant (WorkPackage, WorkPackage.foo.bar(:baz), ...) or passing through an association call whose method name ends in work_packages (project.work_packages, current_user.assigned_work_packages, ...). Foo::WorkPackage and unrelated associations (project.users) are correctly not matched.
  • the value contains a params[...] access (direct or in any descendant)

It autocorrects where(id: X)where_display_id_in(X) when X is a pure params[...] access or || chain of them. Cases like params[:id].to_i flag without an autocorrection (the cast defeats where_display_id_in's smart partition).

# bad
WorkPackage.where(id: params[:work_package_id])
WorkPackage.where(id: params[:work_package_id] || params[:ids])
WorkPackage.includes(:project).where(id: params[:ids])
project.work_packages.where(id: params[:work_package_id])
current_user.assigned_work_packages.where(id: params[:ids])

# good
WorkPackage.where_display_id_in(params[:work_package_id])
project.work_packages.where_display_id_in(params[:work_package_id])

# good (primary key, not user input)
WorkPackage.where(id: 42)

# good (subquery, not user input)
WorkPackage.where(id: other_scope.select(:id))

Verification

Release plan

Marked draft pending opf/openproject#23016 merging (which introduces where_display_id_in, the autocorrect target). Once both land, cut a 0.5.0 release and bump the gem version in opf/openproject in a separate PR.

Test plan

  • bundle exec rake (specs + self-lint) green
  • Cop run end-to-end against opf/openproject app/, modules/, lib/ — exactly the expected offense, no false positives
  • Chained-association receivers (project.work_packages.where(id: params[...])) covered by spec

Catches `WorkPackage.where(id: params[...])` patterns that silently drop
semantic identifiers (e.g. "PROJ-42") because PostgreSQL casts the string
to integer 0 inside `where(id: ...)`. Suggests `where_display_id_in`,
which partitions numeric and semantic inputs and consults the alias
table.

The cop only fires when the receiver chain demonstrably starts at the
`WorkPackage` constant and the value is derived from `params[...]`, so
internal subquery composition (`where(id: scope.pluck(:id))`) and
primary-key literals are left untouched. Autocorrects when the value is
a pure `params[...]` access or `||` chain of them; flags
without-correcting cases like `params[:id].to_i`.

Verified against the full openproject tree (6,746 files): 1 real
offense, 0 false positives.
Extend the work_package_relation? matcher to also recognize chains
passing through an association call whose method name ends in
`work_packages`. Catches:

  project.work_packages.where(id: params[:work_package_id])
  current_user.assigned_work_packages.where(id: params[:ids])
  project.work_packages.includes(:project).where(id: params[:ids])

while leaving unrelated associations (`project.users`, ...) alone.
Verified against opf/openproject again: still 1 real offense, 0 false
positives.
@akabiru akabiru marked this pull request as ready for review May 5, 2026 12:58
@akabiru akabiru requested review from a team and Copilot May 5, 2026 12:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new RuboCop cop to prevent a known class of bugs where user-supplied semantic WorkPackage identifiers (e.g., PROJ-42) are passed to WorkPackage.where(id: ...) and silently fail due to PostgreSQL integer coercion, and guides developers toward where_display_id_in.

Changes:

  • Add OpenProject/NoParamsInWorkPackageWhereId cop with autocorrect to where_display_id_in for direct params[...] (and params[...] || params[...]) inputs.
  • Add a dedicated spec suite covering rooted/chained receivers, association receivers, and non-autocorrectable wrapped params.
  • Bump gem version to 0.5.0 and register the new cop in defaults, require list, and changelog.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/rubocop/cop/open_project/no_params_in_work_package_where_id.rb Implements the new cop, receiver detection, params detection, and autocorrect logic.
spec/rubocop/cop/open_project/no_params_in_work_package_where_id_spec.rb Adds coverage for offense detection and autocorrection behavior across receiver forms.
lib/rubocop/cop/open_project_cops.rb Registers the new cop for loading.
config/default.yml Enables the new cop by default and records VersionAdded.
CHANGELOG.md Documents the newly added cop under Unreleased.
lib/rubocop/open_project/version.rb Bumps gem version to 0.5.0.
Gemfile.lock Updates lockfile to reflect the new gem version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rubocop/cop/open_project/no_params_in_work_package_where_id.rb
Comment thread lib/rubocop/open_project/version.rb
Two fixes from PR review:

- Don't autocorrect `where(id: params[:id], project_id: 5)` to
  `where_display_id_in(params[:id])` — the rewrite would silently drop
  the `project_id:` predicate. Refuse to autocorrect when the hash has
  more than one pair; the offense is still flagged for manual fix.

- Move the changelog entry from `[Unreleased]` to a dated `0.5.0`
  section to match the release-bundling convention from the prior cop
  PR (NoNotImplementedError → 0.4.0). Version was already bumped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants