rubocop-openproject 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cfe3bab3bbbc3032190c71f7e824eb2d451f854248d076aca30db79723f346b
4
- data.tar.gz: b265782c32033cd8315f9a0b3bef3cb4cf7c0bdcf01d3fb85de59559fa9a9935
3
+ metadata.gz: 275bc3f46bfa1dd8ecc461306e5671cadbce64a83a571044d8fff2d08af163a0
4
+ data.tar.gz: 6c492b822185a201a389c3fd035fed0f02b60a44a87497090bf34d57c422af52
5
5
  SHA512:
6
- metadata.gz: cde9e717e29db386ab61514b4533f0861956fe2aefe5eeadc9b5baf0c12fb4fa3851f565c0cab2ca9919ed652c27623a45ef1f41ed9c6f2f9bce81fcf2f9aff7
7
- data.tar.gz: 690ebd432e8719ba28f5bd2e146304883626dde1cdd8fab45a6dc518cf16401615c3cdab7382c258db7cc166b40baac9e512c915a26a59aafe5fbb0897d74d22
6
+ metadata.gz: 65aacad0b8c4b61e690253a4b86169bdac9b598df89da79f98995e113fb14cad688395495863382a023c002c1ef4db101eb1d0ef162be0571c2b078d38443c8f
7
+ data.tar.gz: 0ceddce3d195d754dbdf194de88a68a284e86c099ee6f4d9d2069caef736bfb7b9a1ea9d4d9f1ac17e502321814c87115c197bed15bc47486a17042202b7ede9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2026-05-05
4
+
5
+ - Add `OpenProject/NoParamsInWorkPackageWhereId` cop to catch
6
+ `WorkPackage.where(id: params[...])` patterns that silently drop semantic
7
+ identifiers (e.g. `"PROJ-42"`) when PostgreSQL casts the string to integer 0.
8
+
3
9
  ## [0.4.0] - 2026-03-27
4
10
 
5
11
  - Add NoNotImplementedError cop
data/config/default.yml CHANGED
@@ -13,6 +13,11 @@ OpenProject/NoNotImplementedError:
13
13
  Enabled: true
14
14
  VersionAdded: '0.4.0'
15
15
 
16
+ OpenProject/NoParamsInWorkPackageWhereId:
17
+ Description: 'Avoid `WorkPackage.where(id: params[...])`; use `where_display_id_in` to honour semantic identifiers.'
18
+ Enabled: true
19
+ VersionAdded: '0.5.0'
20
+
16
21
  OpenProject/NoSleepInFeatureSpecs:
17
22
  Description: 'Avoid using `sleep` greater than 1 second in feature specs.'
18
23
  Enabled: true
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module OpenProject
6
+ # Flags `WorkPackage.where(id: params[...])` patterns. With semantic work
7
+ # package identifiers enabled, params may carry strings like "PROJ-42"
8
+ # that PostgreSQL silently casts to integer 0 inside `where(id: ...)`,
9
+ # producing an empty result set instead of an error. Use the dedicated
10
+ # resolver `WorkPackage.where_display_id_in(...)` which partitions
11
+ # numeric and semantic inputs and consults the alias table.
12
+ #
13
+ # The cop fires when the receiver chain demonstrably resolves to a
14
+ # WorkPackage relation — either rooted at the `WorkPackage` constant or
15
+ # passing through an association call whose name ends in
16
+ # `work_packages` (e.g. `project.work_packages`,
17
+ # `user.assigned_work_packages`) — and the value is derived from
18
+ # `params[...]`. Internal subquery composition
19
+ # (`where(id: scope.pluck(:id))`) and primary-key literals are left
20
+ # alone.
21
+ #
22
+ # @example
23
+ # # bad
24
+ # WorkPackage.where(id: params[:work_package_id])
25
+ #
26
+ # # bad
27
+ # WorkPackage.where(id: params[:work_package_id] || params[:ids])
28
+ #
29
+ # # bad
30
+ # WorkPackage.includes(:project).where(id: params[:ids])
31
+ #
32
+ # # bad
33
+ # project.work_packages.where(id: params[:work_package_id])
34
+ #
35
+ # # bad
36
+ # current_user.assigned_work_packages.where(id: params[:ids])
37
+ #
38
+ # # good
39
+ # WorkPackage.where_display_id_in(params[:work_package_id])
40
+ #
41
+ # # good
42
+ # project.work_packages.where_display_id_in(params[:work_package_id])
43
+ #
44
+ # # good (primary key, not user input)
45
+ # WorkPackage.where(id: 42)
46
+ #
47
+ # # good (subquery, not user input)
48
+ # WorkPackage.where(id: other_scope.select(:id))
49
+ class NoParamsInWorkPackageWhereId < Base
50
+ extend AutoCorrector
51
+
52
+ MSG = "Avoid `WorkPackage.where(id: params[...])` — semantic identifiers like " \
53
+ '"PROJ-42" are silently coerced to 0 by the SQL cast. ' \
54
+ "Use `WorkPackage.where_display_id_in(...)` instead."
55
+
56
+ RESTRICT_ON_SEND = %i[where].freeze
57
+
58
+ def_node_matcher :params_access?, <<~PATTERN
59
+ (send (send nil? :params) :[] _)
60
+ PATTERN
61
+
62
+ # A receiver that traces back through any chain of sends to either:
63
+ # - the `WorkPackage` constant: `WorkPackage`, `WorkPackage.foo`, ...
64
+ # - an association call whose name ends in `work_packages`:
65
+ # `project.work_packages`, `user.assigned_work_packages`, ...
66
+ # The recursion handles arbitrarily deep chains in either form.
67
+ def_node_matcher :work_package_relation?, <<~PATTERN
68
+ {
69
+ (const nil? :WorkPackage)
70
+ (send _ #work_package_association? ...)
71
+ (send #work_package_relation? _ ...)
72
+ }
73
+ PATTERN
74
+
75
+ def on_send(node)
76
+ return unless work_package_relation?(node.receiver)
77
+
78
+ hash_arg = node.first_argument
79
+ id_value = id_value_from_hash(hash_arg)
80
+ return unless id_value && value_uses_params?(id_value)
81
+
82
+ add_offense(node) do |corrector|
83
+ next unless autocorrectable_value?(id_value) && sole_id_predicate?(hash_arg)
84
+
85
+ corrector.replace(node, "#{node.receiver.source}.where_display_id_in(#{id_value.source})")
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def id_value_from_hash(arg)
92
+ return unless arg&.hash_type?
93
+
94
+ pair = arg.pairs.find { |p| p.key.sym_type? && p.key.value == :id }
95
+ pair&.value
96
+ end
97
+
98
+ # Refuse to autocorrect when the hash carries additional predicates
99
+ # (e.g. `where(id: params[:id], project_id: 5)`); rewriting to
100
+ # `where_display_id_in(params[:id])` would silently drop them.
101
+ def sole_id_predicate?(hash_arg)
102
+ hash_arg.pairs.size == 1
103
+ end
104
+
105
+ def value_uses_params?(node)
106
+ return true if params_access?(node)
107
+
108
+ node.each_descendant(:send).any? { |descendant| params_access?(descendant) }
109
+ end
110
+
111
+ def autocorrectable_value?(node)
112
+ return true if params_access?(node)
113
+ return false unless node.or_type?
114
+
115
+ node.children.all? { |child| autocorrectable_value?(child) }
116
+ end
117
+
118
+ def work_package_association?(method_name)
119
+ method_name.to_s.end_with?("work_packages")
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -2,5 +2,6 @@
2
2
 
3
3
  require_relative "open_project/add_preview_for_view_component"
4
4
  require_relative "open_project/no_not_implemented_error"
5
+ require_relative "open_project/no_params_in_work_package_where_id"
5
6
  require_relative "open_project/use_service_result_factory_methods"
6
7
  require_relative "open_project/no_sleep_in_feature_specs"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module OpenProject
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-openproject
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenProject GmbH
@@ -40,6 +40,7 @@ files:
40
40
  - lib/rubocop-openproject.rb
41
41
  - lib/rubocop/cop/open_project/add_preview_for_view_component.rb
42
42
  - lib/rubocop/cop/open_project/no_not_implemented_error.rb
43
+ - lib/rubocop/cop/open_project/no_params_in_work_package_where_id.rb
43
44
  - lib/rubocop/cop/open_project/no_sleep_in_feature_specs.rb
44
45
  - lib/rubocop/cop/open_project/use_service_result_factory_methods.rb
45
46
  - lib/rubocop/cop/open_project_cops.rb