rubocop-openproject 0.4.0 → 0.6.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/.ruby-version +1 -0
- data/CHANGELOG.md +13 -1
- data/README.md +10 -7
- data/config/default.yml +10 -0
- data/lib/rubocop/cop/open_project/no_params_in_work_package_where_id.rb +124 -0
- data/lib/rubocop/cop/open_project/use_render_mode_instead_of_primitives.rb +115 -0
- data/lib/rubocop/cop/open_project_cops.rb +2 -0
- data/lib/rubocop/open_project/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 821ee2f0425e32049c380dd582471e37feb47c8565b1a8b05e2746809e498993
|
|
4
|
+
data.tar.gz: b8c1b464b25dbb3f0c07dcd5d7f20fd8bd331b91565f31782c7d27920a6148be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20ea7aec95cb84472a6e145dfc71e31a542a3cda8e6a126ea5f6adf8832c6852aff30d506c624bb930fca6ef4a2fb324ecf777b88a2afb3d7d9e2e227c7a436e
|
|
7
|
+
data.tar.gz: 240bcf2a89496e23e73ca909171b754eb92e417bb86d67c07cf3dc2415ca1404879d59fabff2c829bec58d40484b8c49e833a719749c95a2b1b1828b5b0de294
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
# rubocop-openproject
|
|
2
|
+
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6e7b812: Add `OpenProject/UseRenderModeInsteadOfPrimitives` cop to flag `format_text` calls that pass the external-rendering primitive flags (`static_html: true`, `plain_text: true`, `only_path: false`) instead of the canonical `render_mode:` API or the `format_mail_html` / `format_mail_text` mailer view helpers.
|
|
8
|
+
|
|
9
|
+
## [0.5.0] - 2026-05-05
|
|
10
|
+
|
|
11
|
+
- Add `OpenProject/NoParamsInWorkPackageWhereId` cop to catch
|
|
12
|
+
`WorkPackage.where(id: params[...])` patterns that silently drop semantic
|
|
13
|
+
identifiers (e.g. `"PROJ-42"`) when PostgreSQL casts the string to integer 0.
|
|
2
14
|
|
|
3
15
|
## [0.4.0] - 2026-03-27
|
|
4
16
|
|
data/README.md
CHANGED
|
@@ -24,16 +24,19 @@ require:
|
|
|
24
24
|
|
|
25
25
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
26
26
|
|
|
27
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
27
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
28
|
+
|
|
29
|
+
When working on a change, add a changeset describing it: run `npx changeset`
|
|
30
|
+
(or `./node_modules/.bin/changeset` after `npm install`), pick the bump
|
|
31
|
+
(`patch` / `minor` / `major`), and write a short summary. This adds a file under
|
|
32
|
+
`.changeset/`; commit it with your PR. The release automation turns accumulated
|
|
33
|
+
changesets into the next version bump and `CHANGELOG.md` entry.
|
|
28
34
|
|
|
29
35
|
## Releasing
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
4. Run `rake` to check everything is fine and fix any issues
|
|
35
|
-
5. Commit your changes (`git commit -am 'Prepare release X.Y.Z'`)
|
|
36
|
-
6. Run `bundle exec rake release`
|
|
37
|
+
Releases are driven by [changesets](https://github.com/changesets/changesets) and
|
|
38
|
+
GitHub Actions, so no RubyGems credentials are needed on your machine. See
|
|
39
|
+
[.github/RELEASING.md](.github/RELEASING.md) for the flow and one-time setup.
|
|
37
40
|
|
|
38
41
|
## Contributing
|
|
39
42
|
|
data/config/default.yml
CHANGED
|
@@ -13,11 +13,21 @@ 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
|
|
19
24
|
VersionAdded: '0.1.0'
|
|
20
25
|
|
|
26
|
+
OpenProject/UseRenderModeInsteadOfPrimitives:
|
|
27
|
+
Description: 'Use `render_mode:` instead of `static_html:`/`plain_text:`/`only_path:` on `format_text`.'
|
|
28
|
+
Enabled: true
|
|
29
|
+
VersionAdded: '0.6.0'
|
|
30
|
+
|
|
21
31
|
OpenProject/UseServiceResultFactoryMethods:
|
|
22
32
|
Description: 'Use ServiceResult factory methods instead of ServiceResult.new.'
|
|
23
33
|
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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module OpenProject
|
|
6
|
+
# Flags `format_text` calls that pass the external-rendering primitive
|
|
7
|
+
# flags (`static_html: true`, `plain_text: true`, `only_path: false`)
|
|
8
|
+
# instead of the canonical `render_mode:` API.
|
|
9
|
+
#
|
|
10
|
+
# `format_text` renders trusted Markdown for two distinct audiences:
|
|
11
|
+
# in-app HTML (the default) and external surfaces such as mailer
|
|
12
|
+
# bodies or feeds. The external surface needs absolute URLs and a
|
|
13
|
+
# subset of macros pre-resolved to static HTML / plain text, because
|
|
14
|
+
# the recipient has no JavaScript runtime to hydrate the rest. The
|
|
15
|
+
# primitive flags express that requirement as three loosely coupled
|
|
16
|
+
# toggles, which makes call sites easy to get partially right (and
|
|
17
|
+
# silently wrong — see openproject#74762, where a mailer view skipped
|
|
18
|
+
# `static_html: true` and rendered numeric work package IDs instead of
|
|
19
|
+
# their semantic identifiers).
|
|
20
|
+
#
|
|
21
|
+
# The canonical API is `render_mode: :external_html` /
|
|
22
|
+
# `render_mode: :external_text`, with the mailer view helpers
|
|
23
|
+
# `format_mail_html` and `format_mail_text` as the preferred form
|
|
24
|
+
# inside mailer templates. The cop fires on both `.rb` and `.erb`
|
|
25
|
+
# sources via OpenProject's erb_lint Rubocop bridge.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# # bad
|
|
29
|
+
# format_text("hi", static_html: true, only_path: false)
|
|
30
|
+
#
|
|
31
|
+
# # bad
|
|
32
|
+
# format_text("hi", plain_text: true, only_path: false)
|
|
33
|
+
#
|
|
34
|
+
# # bad
|
|
35
|
+
# format_text("hi", only_path: false)
|
|
36
|
+
#
|
|
37
|
+
# # bad (two-arg form)
|
|
38
|
+
# format_text(@user, :name, only_path: false)
|
|
39
|
+
#
|
|
40
|
+
# # good (canonical API)
|
|
41
|
+
# format_text("hi", render_mode: :external_html)
|
|
42
|
+
#
|
|
43
|
+
# # good (mailer view helper)
|
|
44
|
+
# format_mail_html("hi")
|
|
45
|
+
#
|
|
46
|
+
# # good (in-app default; no external flags)
|
|
47
|
+
# format_text("hi", only_path: true)
|
|
48
|
+
#
|
|
49
|
+
# # good (render_mode present — primitive override is an explicit escape hatch)
|
|
50
|
+
# format_text("hi", render_mode: :external_html, only_path: false)
|
|
51
|
+
class UseRenderModeInsteadOfPrimitives < Base
|
|
52
|
+
MSG_HTML = "Use `render_mode: :external_html` (or `format_mail_html` in mailer views) " \
|
|
53
|
+
"instead of the primitive flags `static_html: true` / `only_path: false`. " \
|
|
54
|
+
"The `render_mode:` API documents intent and bundles the coupled toggles " \
|
|
55
|
+
"that silently produced numeric IDs in watcher notifications (openproject#74762)."
|
|
56
|
+
|
|
57
|
+
MSG_TEXT = "Use `render_mode: :external_text` (or `format_mail_text` in mailer views) " \
|
|
58
|
+
"instead of the primitive flags `plain_text: true` / `only_path: false`. " \
|
|
59
|
+
"The `render_mode:` API documents intent and bundles the coupled toggles " \
|
|
60
|
+
"that silently produced numeric IDs in watcher notifications (openproject#74762)."
|
|
61
|
+
|
|
62
|
+
MSG_ONLY_PATH = "Use `render_mode: :external_html` (or `format_mail_html` in mailer views) " \
|
|
63
|
+
"instead of passing `only_path: false` on its own. The bare flag is " \
|
|
64
|
+
"an under-specified external render that silently produced numeric IDs " \
|
|
65
|
+
"in watcher notifications (openproject#74762)."
|
|
66
|
+
|
|
67
|
+
RESTRICT_ON_SEND = %i[format_text].freeze
|
|
68
|
+
|
|
69
|
+
def on_send(node)
|
|
70
|
+
hash_arg = node.last_argument
|
|
71
|
+
return unless hash_arg&.hash_type?
|
|
72
|
+
|
|
73
|
+
pairs = symbol_keyed_pairs(hash_arg)
|
|
74
|
+
return if render_mode_present?(pairs)
|
|
75
|
+
|
|
76
|
+
static_html = pair_with_value(pairs, :static_html, true)
|
|
77
|
+
plain_text = pair_with_value(pairs, :plain_text, true)
|
|
78
|
+
only_path_false = pair_with_value(pairs, :only_path, false)
|
|
79
|
+
|
|
80
|
+
return unless static_html || plain_text || only_path_false
|
|
81
|
+
|
|
82
|
+
add_offense(node, message: message_for(static_html, plain_text, only_path_false))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def symbol_keyed_pairs(hash_arg)
|
|
88
|
+
hash_arg.pairs.select { |p| p.key.sym_type? }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_mode_present?(pairs)
|
|
92
|
+
pairs.any? { |p| p.key.value == :render_mode }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def pair_with_value(pairs, key, value)
|
|
96
|
+
pairs.find { |p| p.key.value == key && literal_equals?(p.value, value) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def literal_equals?(node, value)
|
|
100
|
+
case value
|
|
101
|
+
when true then node.true_type?
|
|
102
|
+
when false then node.false_type?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def message_for(static_html, plain_text, only_path_false)
|
|
107
|
+
return MSG_HTML if static_html
|
|
108
|
+
return MSG_TEXT if plain_text
|
|
109
|
+
|
|
110
|
+
MSG_ONLY_PATH if only_path_false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -2,5 +2,7 @@
|
|
|
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"
|
|
8
|
+
require_relative "open_project/use_render_mode_instead_of_primitives"
|
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
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OpenProject GmbH
|
|
@@ -32,6 +32,7 @@ extra_rdoc_files: []
|
|
|
32
32
|
files:
|
|
33
33
|
- ".rspec"
|
|
34
34
|
- ".rubocop.yml"
|
|
35
|
+
- ".ruby-version"
|
|
35
36
|
- CHANGELOG.md
|
|
36
37
|
- LICENSE.txt
|
|
37
38
|
- README.md
|
|
@@ -40,7 +41,9 @@ files:
|
|
|
40
41
|
- lib/rubocop-openproject.rb
|
|
41
42
|
- lib/rubocop/cop/open_project/add_preview_for_view_component.rb
|
|
42
43
|
- lib/rubocop/cop/open_project/no_not_implemented_error.rb
|
|
44
|
+
- lib/rubocop/cop/open_project/no_params_in_work_package_where_id.rb
|
|
43
45
|
- lib/rubocop/cop/open_project/no_sleep_in_feature_specs.rb
|
|
46
|
+
- lib/rubocop/cop/open_project/use_render_mode_instead_of_primitives.rb
|
|
44
47
|
- lib/rubocop/cop/open_project/use_service_result_factory_methods.rb
|
|
45
48
|
- lib/rubocop/cop/open_project_cops.rb
|
|
46
49
|
- lib/rubocop/open_project.rb
|
|
@@ -68,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
71
|
- !ruby/object:Gem::Version
|
|
69
72
|
version: '0'
|
|
70
73
|
requirements: []
|
|
71
|
-
rubygems_version:
|
|
74
|
+
rubygems_version: 4.0.6
|
|
72
75
|
specification_version: 4
|
|
73
76
|
summary: RuboCop cops for OpenProject
|
|
74
77
|
test_files: []
|