rubocop-openproject 0.3.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: 216a0869e29d95a92306d0d02e2bc340b7914733f86d56c4f27cce5fbcf8e466
4
- data.tar.gz: d3ca3bc8f408a56843a5cbd7f8d69a8a1eadc6bda76c5ea8139201181a1e0de8
3
+ metadata.gz: 275bc3f46bfa1dd8ecc461306e5671cadbce64a83a571044d8fff2d08af163a0
4
+ data.tar.gz: 6c492b822185a201a389c3fd035fed0f02b60a44a87497090bf34d57c422af52
5
5
  SHA512:
6
- metadata.gz: 7041d99b3774a92ae145c93bf64ec52628aee9b04ebd08b04a3803c2d662f956404893306d8274762d0b1050c2f6787b135cc15f58d514e0d508bc8673487881
7
- data.tar.gz: 989852deeca12ee26502f448d85e660f6539fca125cd5f174650acf2646da07eef463db5aa025c30d46976484730d2bc0cd363d9953af42fb25f688e24fd3541
6
+ metadata.gz: 65aacad0b8c4b61e690253a4b86169bdac9b598df89da79f98995e113fb14cad688395495863382a023c002c1ef4db101eb1d0ef162be0571c2b078d38443c8f
7
+ data.tar.gz: 0ceddce3d195d754dbdf194de88a68a284e86c099ee6f4d9d2069caef736bfb7b9a1ea9d4d9f1ac17e502321814c87115c197bed15bc47486a17042202b7ede9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
9
+ ## [0.4.0] - 2026-03-27
10
+
11
+ - Add NoNotImplementedError cop
12
+
3
13
  ## [0.3.0] - 2025-07-11
4
14
 
5
15
  - Remove redundant NoDoEndBlockWithRSpecCapybaraMatcherInExpect cop
data/config/default.yml CHANGED
@@ -8,6 +8,16 @@ OpenProject/NoDoEndBlockWithRSpecCapybaraMatcherInExpect:
8
8
  Enabled: true
9
9
  VersionAdded: '0.1.0'
10
10
 
11
+ OpenProject/NoNotImplementedError:
12
+ Description: 'Do not raise `NotImplementedError` to signal an unimplemented abstract method.'
13
+ Enabled: true
14
+ VersionAdded: '0.4.0'
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
+
11
21
  OpenProject/NoSleepInFeatureSpecs:
12
22
  Description: 'Avoid using `sleep` greater than 1 second in feature specs.'
13
23
  Enabled: true
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module OpenProject
6
+ # Warns against using a `NotImplementedError` exception when a method
7
+ # should be implemented by a subclass or including module. Ruby's
8
+ # `NotImplementedError` is reserved for platform-specific missing
9
+ # features (e.g., methods depending on `fsync` or `fork`), not for
10
+ # abstract method patterns.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # raise NotImplementedError
15
+ #
16
+ # # bad
17
+ # raise NotImplementedError, "Subclasses must implement #foo"
18
+ #
19
+ # # bad
20
+ # raise NotImplementedError.new("Subclasses must implement #foo")
21
+ #
22
+ # # bad
23
+ # fail NotImplementedError
24
+ #
25
+ # # good
26
+ # raise NotYetImplementedError
27
+ #
28
+ # # good
29
+ # raise SubclassResponsibilityError, "#{self.class} must implement #foo"
30
+ #
31
+ class NoNotImplementedError < Base
32
+ MSG = "Do not raise `NotImplementedError` to signal an unimplemented abstract method. " \
33
+ "Ruby's `NotImplementedError` is reserved for platform-specific missing features. " \
34
+ "Raise a descriptive custom error class instead."
35
+
36
+ RESTRICT_ON_SEND = %i[raise fail].freeze
37
+
38
+ def_node_matcher :raises_not_implemented_error?, <<~PATTERN
39
+ {
40
+ (send nil? {:raise :fail} (const nil? :NotImplementedError) ...)
41
+ (send nil? {:raise :fail} (send (const nil? :NotImplementedError) :new ...) ...)
42
+ }
43
+ PATTERN
44
+
45
+ def on_send(node)
46
+ return unless raises_not_implemented_error?(node)
47
+
48
+ add_offense(node)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "open_project/add_preview_for_view_component"
4
+ require_relative "open_project/no_not_implemented_error"
5
+ require_relative "open_project/no_params_in_work_package_where_id"
4
6
  require_relative "open_project/use_service_result_factory_methods"
5
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.3.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-openproject
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenProject GmbH
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-07-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rubocop
@@ -40,6 +39,8 @@ files:
40
39
  - config/default.yml
41
40
  - lib/rubocop-openproject.rb
42
41
  - lib/rubocop/cop/open_project/add_preview_for_view_component.rb
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
@@ -54,7 +55,6 @@ metadata:
54
55
  source_code_uri: https://github.com/opf/rubocop-openproject
55
56
  changelog_uri: https://github.com/opf/rubocop-openproject/blob/main/CHANGELOG.md
56
57
  rubygems_mfa_required: 'true'
57
- post_install_message:
58
58
  rdoc_options: []
59
59
  require_paths:
60
60
  - lib
@@ -69,8 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
69
  - !ruby/object:Gem::Version
70
70
  version: '0'
71
71
  requirements: []
72
- rubygems_version: 3.5.11
73
- signing_key:
72
+ rubygems_version: 3.6.9
74
73
  specification_version: 4
75
74
  summary: RuboCop cops for OpenProject
76
75
  test_files: []