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 +4 -4
- data/CHANGELOG.md +10 -0
- data/config/default.yml +10 -0
- data/lib/rubocop/cop/open_project/no_not_implemented_error.rb +53 -0
- data/lib/rubocop/cop/open_project/no_params_in_work_package_where_id.rb +124 -0
- data/lib/rubocop/cop/open_project_cops.rb +2 -0
- data/lib/rubocop/open_project/version.rb +1 -1
- metadata +5 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 275bc3f46bfa1dd8ecc461306e5671cadbce64a83a571044d8fff2d08af163a0
|
|
4
|
+
data.tar.gz: 6c492b822185a201a389c3fd035fed0f02b60a44a87497090bf34d57c422af52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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"
|
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.
|
|
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:
|
|
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.
|
|
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: []
|