rubocop-openproject 0.5.0 → 0.7.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: 275bc3f46bfa1dd8ecc461306e5671cadbce64a83a571044d8fff2d08af163a0
4
- data.tar.gz: 6c492b822185a201a389c3fd035fed0f02b60a44a87497090bf34d57c422af52
3
+ metadata.gz: b6399b37d75f31159de26fc061439ab6107f3621da14770ab2e3f4a263e4748e
4
+ data.tar.gz: c0bc045863a214c19a10fe29e4b23edd6d60f3598158f665fdfb4fefba851946
5
5
  SHA512:
6
- metadata.gz: 65aacad0b8c4b61e690253a4b86169bdac9b598df89da79f98995e113fb14cad688395495863382a023c002c1ef4db101eb1d0ef162be0571c2b078d38443c8f
7
- data.tar.gz: 0ceddce3d195d754dbdf194de88a68a284e86c099ee6f4d9d2069caef736bfb7b9a1ea9d4d9f1ac17e502321814c87115c197bed15bc47486a17042202b7ede9
6
+ metadata.gz: 5089a09294d79954f16b4fe511fd6b3aabe57b9272ebf41fa4a376feb7a6fcf8c2872b072fd6e3c7eb8493484e0e5b3eb2dc88f2bbfb964fa5fb67f0cdd8e5d8
7
+ data.tar.gz: 46759bc28c398aeefb4fb18305dd9494056a5296a859bb7789d4c37548b785a935a254f0cfcc98442cccceeb70fc5a788113316b3c58707dc0172506ab6462e0
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.6
data/CHANGELOG.md CHANGED
@@ -1,4 +1,23 @@
1
- ## [Unreleased]
1
+ # rubocop-openproject
2
+
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ad3ff98: Add `OpenProject/UseEffectiveTypeForConfiguration` cop to catch reading a type's
8
+ configuration off `#type` instead of `#effective_type`. A work package stores the root of its
9
+ type family while its project may resolve that family to a variant configured differently, so
10
+ reading an aspect (form configuration, workflows, custom fields, defaults, export templates)
11
+ off the stored type silently answers with the root's configuration. The failure is quiet — a
12
+ project running the root behaves correctly, so a spec written without a variant passes while
13
+ the feature is broken wherever a variant is resolved. Inherited core settings (`name`,
14
+ `color`, `is_milestone`, `is_in_roadmap`) are read from the root by design and are not flagged.
15
+
16
+ ## 0.6.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 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.
2
21
 
3
22
  ## [0.5.0] - 2026-05-05
4
23
 
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`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
- 1. Update the version in `lib/rubocop/open_project/version.rb`
32
- 2. Update the changelog in `CHANGELOG.md`
33
- 3. Run `bundle install` to update `Gemfile.lock`
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
@@ -23,6 +23,16 @@ OpenProject/NoSleepInFeatureSpecs:
23
23
  Enabled: true
24
24
  VersionAdded: '0.1.0'
25
25
 
26
+ OpenProject/UseEffectiveTypeForConfiguration:
27
+ Description: 'Read a type''s configuration through `effective_type`, not `type`, so the project''s variant is honoured.'
28
+ Enabled: true
29
+ VersionAdded: '0.7.0'
30
+
31
+ OpenProject/UseRenderModeInsteadOfPrimitives:
32
+ Description: 'Use `render_mode:` instead of `static_html:`/`plain_text:`/`only_path:` on `format_text`.'
33
+ Enabled: true
34
+ VersionAdded: '0.6.0'
35
+
26
36
  OpenProject/UseServiceResultFactoryMethods:
27
37
  Description: 'Use ServiceResult factory methods instead of ServiceResult.new.'
28
38
  Enabled: true
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module OpenProject
6
+ # Flags reading a type's configuration off `#type` rather than `#effective_type`.
7
+ #
8
+ # A work package stores the root of its type family, while the project it lives in may
9
+ # resolve that family to a variant configured differently — a different form
10
+ # configuration, workflow, set of custom fields, defaults or export templates. Reading a
11
+ # configuration aspect off the stored type therefore answers with the root's
12
+ # configuration and silently ignores the variant.
13
+ #
14
+ # The failure mode is quiet: a project running the root behaves correctly, so a spec
15
+ # written without a variant passes while the feature is broken for every project that
16
+ # resolves one. That is what this cop is here to catch.
17
+ #
18
+ # Only configuration aspects are flagged. `name`, `color`, `is_milestone` and
19
+ # `is_in_roadmap` are inherited from the root by design, so reading those off `#type` is
20
+ # correct and left alone.
21
+ #
22
+ # Deliberately narrow: it fires when the receiver is literally a `type` call, which is
23
+ # the shape the mistake takes in practice. Administration code holding an explicit type
24
+ # (`@type.attribute_groups`) is reading that member's own configuration on purpose and is
25
+ # not flagged, and neither is a type held in a local variable.
26
+ #
27
+ # @example
28
+ # # bad
29
+ # work_package.type.attribute_groups
30
+ #
31
+ # # bad
32
+ # work_package.type.custom_fields
33
+ #
34
+ # # bad — inside WorkPackage itself
35
+ # type.statuses(include_default: true)
36
+ #
37
+ # # good
38
+ # work_package.effective_type.attribute_groups
39
+ #
40
+ # # good — when there is no work package to ask
41
+ # project.effective_type(type).attribute_groups
42
+ #
43
+ # # good — inherited from the root by design
44
+ # work_package.type.name
45
+ # work_package.type.is_milestone?
46
+ #
47
+ # # good — administration reads a specific member's own configuration
48
+ # @type.attribute_groups
49
+ class UseEffectiveTypeForConfiguration < Base
50
+ MSG = "Read configuration through `effective_type`, not `type` — the stored type is " \
51
+ "the family's root, so this ignores the variant the project resolves to."
52
+
53
+ # Configuration aspects, per Type::ConfigurationLink::ASPECTS and the readers
54
+ # Type::ConfigurationLinkable resolves through the link chain.
55
+ CONFIGURATION_METHODS = %i[
56
+ artefact_export_enabled?
57
+ artefact_export_mode
58
+ attribute_groups
59
+ custom_field_ids
60
+ custom_fields
61
+ description
62
+ export_templates_disabled
63
+ export_templates_order
64
+ patterns
65
+ project_custom_field_type_mappings
66
+ statuses
67
+ workflows
68
+ ].freeze
69
+
70
+ RESTRICT_ON_SEND = CONFIGURATION_METHODS
71
+
72
+ def_node_matcher :type_call?, <<~PATTERN
73
+ (send _ :type)
74
+ PATTERN
75
+
76
+ def on_send(node)
77
+ return unless type_call?(node.receiver)
78
+
79
+ add_offense(node)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ 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
@@ -5,3 +5,5 @@ require_relative "open_project/no_not_implemented_error"
5
5
  require_relative "open_project/no_params_in_work_package_where_id"
6
6
  require_relative "open_project/use_service_result_factory_methods"
7
7
  require_relative "open_project/no_sleep_in_feature_specs"
8
+ require_relative "open_project/use_effective_type_for_configuration"
9
+ require_relative "open_project/use_render_mode_instead_of_primitives"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module OpenProject
5
- VERSION = "0.5.0"
5
+ VERSION = "0.7.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.5.0
4
+ version: 0.7.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
@@ -42,6 +43,8 @@ files:
42
43
  - lib/rubocop/cop/open_project/no_not_implemented_error.rb
43
44
  - lib/rubocop/cop/open_project/no_params_in_work_package_where_id.rb
44
45
  - lib/rubocop/cop/open_project/no_sleep_in_feature_specs.rb
46
+ - lib/rubocop/cop/open_project/use_effective_type_for_configuration.rb
47
+ - lib/rubocop/cop/open_project/use_render_mode_instead_of_primitives.rb
45
48
  - lib/rubocop/cop/open_project/use_service_result_factory_methods.rb
46
49
  - lib/rubocop/cop/open_project_cops.rb
47
50
  - lib/rubocop/open_project.rb
@@ -69,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
72
  - !ruby/object:Gem::Version
70
73
  version: '0'
71
74
  requirements: []
72
- rubygems_version: 3.6.9
75
+ rubygems_version: 4.0.16
73
76
  specification_version: 4
74
77
  summary: RuboCop cops for OpenProject
75
78
  test_files: []