rubocop-gusto 11.2.0 → 11.8.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 +62 -0
- data/README.md +42 -0
- data/config/default.yml +10 -0
- data/config/graphql.yml +44 -0
- data/config/gusto_cops.yml +70 -0
- data/config/obsoletion.yml +24 -0
- data/config/rails.yml +3 -0
- data/config/sorbet.yml +7 -0
- data/lib/rubocop/cop/gusto/constant_safety.rb +41 -0
- data/lib/rubocop/cop/gusto/feature_flag_constants.rb +35 -0
- data/lib/rubocop/cop/gusto/graphql/id_description_concerns.rb +155 -0
- data/lib/rubocop/cop/gusto/graphql/id_field_argument_description.rb +71 -0
- data/lib/rubocop/cop/gusto/graphql/id_field_description.rb +69 -0
- data/lib/rubocop/cop/gusto/graphql/id_input_argument_description.rb +77 -0
- data/lib/rubocop/cop/gusto/graphql/paginate_arrays.rb +25 -0
- data/lib/rubocop/cop/gusto/graphql/prevent_context_hash_access.rb +61 -0
- data/lib/rubocop/cop/gusto/graphql/prevent_field_can_can_action.rb +26 -0
- data/lib/rubocop/cop/gusto/graphql/resolver_ignores_object.rb +172 -0
- data/lib/rubocop/cop/gusto/graphql/type_variables.rb +26 -0
- data/lib/rubocop/cop/gusto/redundant_spec_helper_require.rb +152 -0
- data/lib/rubocop/cop/gusto/sorbet/predicate_boolean_return.rb +280 -0
- data/lib/rubocop/cop/gusto/sudden_associations.rb +47 -0
- data/lib/rubocop/cop/gusto/unreferenced_let.rb +4 -22
- data/lib/rubocop/gusto/init.rb +31 -5
- data/lib/rubocop/gusto/plugin.rb +4 -0
- data/lib/rubocop/gusto/version.rb +1 -1
- metadata +21 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42abd3d1b5e56856b77781b5554a87abea61c419665b553d7bc61053203383cb
|
|
4
|
+
data.tar.gz: 5989d13ccea81744e6a44524003952a304df90e054a2a64ca572ec50d6e2d473
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f77ba3fdfcc4ad8fe904599840fc2388baf724b826e0722544d724e9f679b1e5ea2b00cb99f184a3b5b8ae45efccec0b6466cf87c43654ba33b7492f9ffd524
|
|
7
|
+
data.tar.gz: 64f68fa5679a0207ccd9f843447349211cc65f617a08cdd636e5903987c758945eae9317feea61f8a9286d030b3fbf2c3a4fc99168805a85d68097e79acf1672
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,68 @@
|
|
|
3
3
|
- Remove redundant `Rails: Enabled: true` from `config/rails.yml` (already set by rubocop-rails' own defaults)
|
|
4
4
|
- Enable `Rails/DefaultScope` cop (disabled by default in rubocop-rails)
|
|
5
5
|
|
|
6
|
+
## [11.8.0](https://github.com/Gusto/rubocop-gusto/compare/v11.7.1...v11.8.0) (2026-09-02)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add the Gusto/Graphql cop department, backed by rubocop-graphql ([#170](https://github.com/Gusto/rubocop-gusto/issues/170)) ([98cd994](https://github.com/Gusto/rubocop-gusto/commit/98cd99467d1c50a4c3fb50f48f1dfa0e2905f6b4))
|
|
12
|
+
|
|
13
|
+
## [11.7.1](https://github.com/Gusto/rubocop-gusto/compare/v11.7.0...v11.7.1) (2026-08-17)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* scope Gusto/VcrRecordings to specs ([#161](https://github.com/Gusto/rubocop-gusto/issues/161)) ([923d1b1](https://github.com/Gusto/rubocop-gusto/commit/923d1b1ab4b2af56a6e786afe13755aefdff502e))
|
|
19
|
+
|
|
20
|
+
## [11.7.0](https://github.com/Gusto/rubocop-gusto/compare/v11.6.1...v11.7.0) (2026-08-11)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* add Gusto/Sorbet/PredicateBooleanReturn (opt-in) and Gusto/SuddenAssociations cops ([#157](https://github.com/Gusto/rubocop-gusto/issues/157)) ([f123bbb](https://github.com/Gusto/rubocop-gusto/commit/f123bbbcafdf5b2e38339fa6c541444b4902eb9c))
|
|
26
|
+
|
|
27
|
+
## [11.6.1](https://github.com/Gusto/rubocop-gusto/compare/v11.6.0...v11.6.1) (2026-07-27)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* allow serialize/from_hash/insert_record overrides in Style/OptionalBooleanParameter ([#152](https://github.com/Gusto/rubocop-gusto/issues/152)) ([28e5a3e](https://github.com/Gusto/rubocop-gusto/commit/28e5a3eaa78b592d724a8d3b3044ef847d81e3dc))
|
|
33
|
+
|
|
34
|
+
## [11.6.0](https://github.com/Gusto/rubocop-gusto/compare/v11.5.0...v11.6.0) (2026-07-22)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
* enable Sorbet/RedundantTLetForLiteral and bump rubocop-sorbet for RedundantTLet cops ([#154](https://github.com/Gusto/rubocop-gusto/issues/154)) ([3098b7e](https://github.com/Gusto/rubocop-gusto/commit/3098b7e413c53831354c4c9e8e5337ab743d2a27))
|
|
40
|
+
* enable Sorbet/RedundantTLetForLiteral, bump rubocop-sorbet for RedundantTLet cops ([3098b7e](https://github.com/Gusto/rubocop-gusto/commit/3098b7e413c53831354c4c9e8e5337ab743d2a27))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Bug Fixes
|
|
44
|
+
|
|
45
|
+
* remove git dependency from Gusto/UnreferencedLet ([#153](https://github.com/Gusto/rubocop-gusto/issues/153)) ([933f5b0](https://github.com/Gusto/rubocop-gusto/commit/933f5b0ec86b422caf0d6e6d460c468e8c00c408))
|
|
46
|
+
|
|
47
|
+
## [11.5.0](https://github.com/Gusto/rubocop-gusto/compare/v11.4.0...v11.5.0) (2026-07-14)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
### Features
|
|
51
|
+
|
|
52
|
+
* add Gusto/RedundantSpecHelperRequire cop (ADR 116, with autofix) ([#149](https://github.com/Gusto/rubocop-gusto/issues/149)) ([83c22c9](https://github.com/Gusto/rubocop-gusto/commit/83c22c9ee4fb0bc8a5faff11769570009892cccd))
|
|
53
|
+
|
|
54
|
+
## [11.4.0](https://github.com/Gusto/rubocop-gusto/compare/v11.3.0...v11.4.0) (2026-07-13)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### Features
|
|
58
|
+
|
|
59
|
+
* add Gusto/ConstantSafety cop ([#148](https://github.com/Gusto/rubocop-gusto/issues/148)) ([7ee7925](https://github.com/Gusto/rubocop-gusto/commit/7ee7925094a7df531d1a9eb078e474fa89b3c133))
|
|
60
|
+
|
|
61
|
+
## [11.3.0](https://github.com/Gusto/rubocop-gusto/compare/v11.2.0...v11.3.0) (2026-06-29)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### Features
|
|
65
|
+
|
|
66
|
+
* add Gusto/FeatureFlagConstants cop (RR-890) ([#144](https://github.com/Gusto/rubocop-gusto/issues/144)) ([9058e0e](https://github.com/Gusto/rubocop-gusto/commit/9058e0e3055445a6be1818f2c142013239773bb5))
|
|
67
|
+
|
|
6
68
|
## [11.2.0](https://github.com/Gusto/rubocop-gusto/compare/v11.1.1...v11.2.0) (2026-06-26)
|
|
7
69
|
|
|
8
70
|
|
data/README.md
CHANGED
|
@@ -32,6 +32,47 @@ This adds `rubocop-gusto` to your `.rubocop.yml` `plugins:` list and includes an
|
|
|
32
32
|
|
|
33
33
|
If this is an existing project, it is recommended to run the autocorrector (`bundle exec rubocop -a`) and then to regenerate the `.rubocop_todo.yml` (`bundle exec rubocop --auto-gen-config`), so issues can be dealt with piecemeal.
|
|
34
34
|
|
|
35
|
+
#### GraphQL configuration
|
|
36
|
+
|
|
37
|
+
The `Gusto/Graphql` cops lint the [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) schema
|
|
38
|
+
DSL. They live in `config/graphql.yml`, which also configures the `GraphQL` cops from
|
|
39
|
+
[rubocop-graphql](https://github.com/DmitryTsepelev/rubocop-graphql) — several of them cops Gusto
|
|
40
|
+
contributed upstream rather than keep here. Neither is in `config/default.yml`, so projects without
|
|
41
|
+
a GraphQL schema are not linted for those patterns.
|
|
42
|
+
|
|
43
|
+
Running `bundle exec rubocop-gusto init` does all of it when `graphql` is in your `Gemfile` or
|
|
44
|
+
`Gemfile.lock`: adds `rubocop-graphql` to your Gemfile and `plugins:` list, and adds
|
|
45
|
+
`config/graphql.yml` to your `inherit_gem` list. rubocop-graphql is deliberately *not* a dependency
|
|
46
|
+
of this gem, for the same reason rubocop-rails is not — so rubocop-gusto stays usable from a plain
|
|
47
|
+
gem.
|
|
48
|
+
|
|
49
|
+
`GraphQL/DisallowedTypes` is where a project names the types it has decided not to expose. It is
|
|
50
|
+
inert until configured, and the reason is per type, so it needs no `Details:`:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
GraphQL/DisallowedTypes:
|
|
54
|
+
Types:
|
|
55
|
+
Float: 'Use MyApp::GraphQL::Scalars::Decimal, which serializes as a string.'
|
|
56
|
+
MyApp::Scalars::LegacyDate: 'Use GraphQL::Types::ISO8601Date.'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
By default the `Gusto/Graphql` department is scoped to `**/graphql/**/*`.
|
|
60
|
+
`Gusto/Graphql/ResolverIgnoresObject` reads `NodeAccessors`, which lists methods your GraphQL base
|
|
61
|
+
classes provide that read the node without naming `object`; leave it empty and the cop reports
|
|
62
|
+
those resolvers as offenses.
|
|
63
|
+
|
|
64
|
+
Several of these cops replace a project-specific convention with a generic message. Put the
|
|
65
|
+
convention your project actually follows in the cop's `Details:`, and set
|
|
66
|
+
`AllCops: ExtraDetails: true` so it is appended to the offense message:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
AllCops:
|
|
70
|
+
ExtraDetails: true
|
|
71
|
+
|
|
72
|
+
Gusto/Graphql/PaginateArrays:
|
|
73
|
+
Details: 'Use `paginated_list(<Type>)`, or `non_paginated_list(<Type>)` for a bounded list.'
|
|
74
|
+
```
|
|
75
|
+
|
|
35
76
|
#### Sidekiq configuration
|
|
36
77
|
|
|
37
78
|
Sidekiq-specific cops live in `config/sidekiq.yml` and are **not** included in `config/default.yml`, so projects without Sidekiq are not linted for those patterns. Running `bundle exec rubocop-gusto init` adds `config/sidekiq.yml` to your `inherit_gem` list automatically when Sidekiq is listed in your `Gemfile` or `Gemfile.lock`.
|
|
@@ -52,6 +93,7 @@ If your project also uses Rails, include `config/rails.yml` as well (order does
|
|
|
52
93
|
Custom cops live under the following namespaces:
|
|
53
94
|
|
|
54
95
|
- `Gusto/` — general Gusto-specific cops (see [`lib/rubocop/cop/gusto/`](lib/rubocop/cop/gusto/))
|
|
96
|
+
- `Gusto/Graphql/` — cops scoped to the graphql-ruby schema DSL (see [`lib/rubocop/cop/gusto/graphql/`](lib/rubocop/cop/gusto/graphql/)); configured in [`config/graphql.yml`](config/graphql.yml)
|
|
55
97
|
- `Sidekiq/` — cops scoped to Sidekiq patterns (see [`lib/rubocop/cop/sidekiq/`](lib/rubocop/cop/sidekiq/)); configured in [`config/sidekiq.yml`](config/sidekiq.yml)
|
|
56
98
|
- `Rack/` — cops scoped to Rack middleware patterns (see [`lib/rubocop/cop/rack/`](lib/rubocop/cop/rack/))
|
|
57
99
|
|
data/config/default.yml
CHANGED
|
@@ -274,6 +274,11 @@ Sorbet/ForbidTUnsafe:
|
|
|
274
274
|
# reorganizing the code over silently turning off the type checker.
|
|
275
275
|
Enabled: true
|
|
276
276
|
|
|
277
|
+
Sorbet/RedundantTLetForLiteral:
|
|
278
|
+
# Still marked pending upstream, we'd like to enable it now. Remove this
|
|
279
|
+
# override once it's no longer pending in rubocop-sorbet.
|
|
280
|
+
Enabled: true
|
|
281
|
+
|
|
277
282
|
Sorbet/Refinement:
|
|
278
283
|
# Still marked pending upstream, we contributed this and enable it here.
|
|
279
284
|
Enabled: true
|
|
@@ -498,6 +503,11 @@ Style/OptionalBooleanParameter:
|
|
|
498
503
|
AllowedMethods:
|
|
499
504
|
- respond_to_missing?
|
|
500
505
|
- perform # Sidekiq does not support keyword arguments.
|
|
506
|
+
- serialize # Overrides Sorbet's T::Props::Serializable#serialize(strict=true).
|
|
507
|
+
- from_hash # Overrides Sorbet's T::Props::Serializable#from_hash(hash, strict=false).
|
|
508
|
+
- insert_record # Overrides Rails' ActiveRecord::Associations::HasManyThroughAssociation#insert_record.
|
|
509
|
+
- lock # Overrides Rails' ActiveRecord::QueryMethods#lock(locks=true).
|
|
510
|
+
- lock! # Overrides Rails' ActiveRecord::Locking::Pessimistic#lock!(lock=true).
|
|
501
511
|
|
|
502
512
|
Style/PercentLiteralDelimiters:
|
|
503
513
|
PreferredDelimiters:
|
data/config/graphql.yml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file should be inherited alongside default.yml for projects using graphql-ruby
|
|
3
|
+
#
|
|
4
|
+
# It configures both the `Gusto/Graphql` cops in this gem and the `GraphQL` cops from
|
|
5
|
+
# rubocop-graphql, which `init` adds to the project's Gemfile and `plugins:` list.
|
|
6
|
+
#
|
|
7
|
+
# After you add a rule, sort this file with `bundle exec rubocop-gusto sort config/graphql.yml`
|
|
8
|
+
|
|
9
|
+
GraphQL/DefaultForOptionalArgument:
|
|
10
|
+
Enabled: true
|
|
11
|
+
|
|
12
|
+
GraphQL/DisallowedTypes:
|
|
13
|
+
Enabled: true
|
|
14
|
+
# Nothing is disallowed until a project fills this in; the keys are matched against the
|
|
15
|
+
# written constant exactly or as a trailing segment, so `Float` also covers
|
|
16
|
+
# `GraphQL::Types::Float`.
|
|
17
|
+
Types: {}
|
|
18
|
+
|
|
19
|
+
GraphQL/NullabilityMismatch:
|
|
20
|
+
Enabled: true
|
|
21
|
+
|
|
22
|
+
Gusto/Graphql/IdFieldArgumentDescription:
|
|
23
|
+
Enabled: true
|
|
24
|
+
|
|
25
|
+
Gusto/Graphql/IdFieldDescription:
|
|
26
|
+
Enabled: true
|
|
27
|
+
|
|
28
|
+
Gusto/Graphql/IdInputArgumentDescription:
|
|
29
|
+
Enabled: true
|
|
30
|
+
|
|
31
|
+
Gusto/Graphql/PaginateArrays:
|
|
32
|
+
Enabled: true
|
|
33
|
+
|
|
34
|
+
Gusto/Graphql/PreventContextHashAccess:
|
|
35
|
+
Enabled: true
|
|
36
|
+
|
|
37
|
+
Gusto/Graphql/PreventFieldCanCanAction:
|
|
38
|
+
Enabled: true
|
|
39
|
+
|
|
40
|
+
Gusto/Graphql/ResolverIgnoresObject:
|
|
41
|
+
Enabled: true
|
|
42
|
+
|
|
43
|
+
Gusto/Graphql/TypeVariables:
|
|
44
|
+
Enabled: true
|
data/config/gusto_cops.yml
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
Gusto/BootsnapLoadFile:
|
|
13
13
|
Description: 'Do not use Bootsnap to load files. Use `require` instead.'
|
|
14
14
|
|
|
15
|
+
Gusto/ConstantSafety:
|
|
16
|
+
Description: 'Flags safe navigation (`&.`) on a constant (class/module or SCREAMING_CASE); constants are never nil, so `.` should be used instead.'
|
|
17
|
+
|
|
15
18
|
Gusto/DatadogConstant:
|
|
16
19
|
Exclude:
|
|
17
20
|
# calling DataDog directly only allowed in initializers, its library, and tests
|
|
@@ -43,6 +46,50 @@ Gusto/FactoryClassesOrModules:
|
|
|
43
46
|
Include:
|
|
44
47
|
- 'spec/**/factories/*.rb'
|
|
45
48
|
|
|
49
|
+
Gusto/FeatureFlagConstants:
|
|
50
|
+
Description: 'FeatureFlag keys should be constants, not strings.'
|
|
51
|
+
|
|
52
|
+
Gusto/Graphql:
|
|
53
|
+
# GraphQL code conventionally lives under a `graphql` directory. Projects that lay it out
|
|
54
|
+
# differently should override this Include rather than each cop's.
|
|
55
|
+
Include:
|
|
56
|
+
- '**/graphql/**/*'
|
|
57
|
+
|
|
58
|
+
Gusto/Graphql/IdFieldArgumentDescription:
|
|
59
|
+
Description: 'Requires ID arguments on query fields to follow the standard description template.'
|
|
60
|
+
Enabled: false
|
|
61
|
+
|
|
62
|
+
Gusto/Graphql/IdFieldDescription:
|
|
63
|
+
Description: 'Requires ID fields to follow the standard description template.'
|
|
64
|
+
Enabled: false
|
|
65
|
+
|
|
66
|
+
Gusto/Graphql/IdInputArgumentDescription:
|
|
67
|
+
Description: 'Requires ID arguments on mutations and input objects to follow the standard description template.'
|
|
68
|
+
Enabled: false
|
|
69
|
+
|
|
70
|
+
Gusto/Graphql/PaginateArrays:
|
|
71
|
+
Description: 'Flags a GraphQL field typed as an unbounded array rather than a paginated list.'
|
|
72
|
+
Enabled: false
|
|
73
|
+
|
|
74
|
+
Gusto/Graphql/PreventContextHashAccess:
|
|
75
|
+
Description: 'Disallows hash access on the GraphQL context; use typed context methods instead.'
|
|
76
|
+
Enabled: false
|
|
77
|
+
|
|
78
|
+
Gusto/Graphql/PreventFieldCanCanAction:
|
|
79
|
+
Description: 'Flags the deprecated `can_can_action:` option on a GraphQL field declaration.'
|
|
80
|
+
Enabled: false
|
|
81
|
+
|
|
82
|
+
Gusto/Graphql/ResolverIgnoresObject:
|
|
83
|
+
Description: 'Flags a GraphQL field whose resolver never reads `object`, so the value is the same for every node.'
|
|
84
|
+
Enabled: false
|
|
85
|
+
# Methods inherited from a base class that read the node without naming `object`. A project
|
|
86
|
+
# whose GraphQL bases offer such helpers must list them here or the cop reports false positives.
|
|
87
|
+
NodeAccessors: []
|
|
88
|
+
|
|
89
|
+
Gusto/Graphql/TypeVariables:
|
|
90
|
+
Description: 'Requires `type_member` and `type_template` in GraphQL objects to name a type rather than `T.untyped`.'
|
|
91
|
+
Enabled: false
|
|
92
|
+
|
|
46
93
|
Gusto/MinByMaxBy:
|
|
47
94
|
Description: 'Checks for the use of `min` or `max` with a proc. Corrects to `min_by` or `max_by`.'
|
|
48
95
|
Safe: false
|
|
@@ -89,6 +136,12 @@ Gusto/RakeConstants:
|
|
|
89
136
|
- '**/*.rake'
|
|
90
137
|
- 'Rakefile'
|
|
91
138
|
|
|
139
|
+
Gusto/RedundantSpecHelperRequire:
|
|
140
|
+
Description: "Removes an inline require 'spec_helper'/'rails_helper' already auto-required by the governing .rspec."
|
|
141
|
+
Include:
|
|
142
|
+
- '**/spec/**/*'
|
|
143
|
+
SafeAutoCorrect: false
|
|
144
|
+
|
|
92
145
|
Gusto/RegexpBypass:
|
|
93
146
|
Description: 'Ensures regular expressions use \A and \z anchors instead of ^ and $ for security validation.'
|
|
94
147
|
Exclude:
|
|
@@ -108,6 +161,21 @@ Gusto/SmartTodoTeam:
|
|
|
108
161
|
Description: 'TODO comments must be SmartTodo-formatted and target a valid team (CodeTeams).'
|
|
109
162
|
Enabled: false
|
|
110
163
|
|
|
164
|
+
Gusto/Sorbet/PredicateBooleanReturn:
|
|
165
|
+
Description: 'Predicate methods in Sorbet-typed files should return T::Boolean.'
|
|
166
|
+
# Opted into via config/sorbet.yml when the project depends on Sorbet.
|
|
167
|
+
Enabled: false
|
|
168
|
+
# Autocorrect rewrites the signature and coerces the return value with `!!()`,
|
|
169
|
+
# which can change behavior in subtle ways.
|
|
170
|
+
SafeAutoCorrect: false
|
|
171
|
+
|
|
172
|
+
Gusto/SuddenAssociations:
|
|
173
|
+
Description: 'Prevents defining Rails associations on another class from outside the class body.'
|
|
174
|
+
# Opted into via config/rails.yml; the association DSL only exists in a Rails project.
|
|
175
|
+
Enabled: false
|
|
176
|
+
Exclude:
|
|
177
|
+
- '**/spec/**/*' # allow stubbing, although this is still odd
|
|
178
|
+
|
|
111
179
|
Gusto/ToplevelConstants:
|
|
112
180
|
Description: 'Prevents top-level constants from being defined outside of initializers.'
|
|
113
181
|
Include:
|
|
@@ -137,6 +205,8 @@ Gusto/UsePaintNotColorize:
|
|
|
137
205
|
|
|
138
206
|
Gusto/VcrRecordings:
|
|
139
207
|
Description: 'VCR should be set to not record in tests. Use vcr: {record: :none}.'
|
|
208
|
+
Include:
|
|
209
|
+
- '**/spec/**/*'
|
|
140
210
|
|
|
141
211
|
# We extend this via Gusto/SmartTodoTeam; don't let the upstream cop run standalone.
|
|
142
212
|
SmartTodo/SmartTodoCop:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Obsolete configuration reported by `ConfigObsoletion`, registered from
|
|
2
|
+
# `RuboCop::Gusto::Plugin#rules`.
|
|
3
|
+
#
|
|
4
|
+
# The `Graphql/*` department was a private, in-repo department before these cops moved into
|
|
5
|
+
# this gem, where they live in the `Gusto/Graphql` sub-department. `Graphql` also sits one
|
|
6
|
+
# capital letter away from `GraphQL`, the department shipped by rubocop-graphql, so keeping
|
|
7
|
+
# the old spelling would have left two near-identical departments in one config.
|
|
8
|
+
#
|
|
9
|
+
# Four of them were contributed to rubocop-graphql itself and are not duplicated here, so
|
|
10
|
+
# they point into that gem's `GraphQL` department instead. `PreventFloat` and
|
|
11
|
+
# `PreventScalarsDate` were generalized into one config-driven `GraphQL/DisallowedTypes`.
|
|
12
|
+
renamed:
|
|
13
|
+
Graphql/DefaultForOptionalArgument: GraphQL/DefaultForOptionalArgument
|
|
14
|
+
Graphql/IdFieldArgumentDescription: Gusto/Graphql/IdFieldArgumentDescription
|
|
15
|
+
Graphql/IdFieldDescription: Gusto/Graphql/IdFieldDescription
|
|
16
|
+
Graphql/IdInputArgumentDescription: Gusto/Graphql/IdInputArgumentDescription
|
|
17
|
+
Graphql/NullabilityMismatch: GraphQL/NullabilityMismatch
|
|
18
|
+
Graphql/PaginateArrays: Gusto/Graphql/PaginateArrays
|
|
19
|
+
Graphql/PreventContextHashAccess: Gusto/Graphql/PreventContextHashAccess
|
|
20
|
+
Graphql/PreventFieldCanCanAction: Gusto/Graphql/PreventFieldCanCanAction
|
|
21
|
+
Graphql/PreventFloat: GraphQL/DisallowedTypes
|
|
22
|
+
Graphql/PreventScalarsDate: GraphQL/DisallowedTypes
|
|
23
|
+
Graphql/ResolverIgnoresObject: Gusto/Graphql/ResolverIgnoresObject
|
|
24
|
+
Graphql/TypeVariables: Gusto/Graphql/TypeVariables
|
data/config/rails.yml
CHANGED
data/config/sorbet.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Gusto
|
|
6
|
+
# Flags safe navigation (`&.`) called on a constant, covering both
|
|
7
|
+
# class/module constants (`Model&.find`) and SCREAMING_CASE constants
|
|
8
|
+
# (`CONST&.each`).
|
|
9
|
+
#
|
|
10
|
+
# A constant reference is never `nil`, an undefined constant raises
|
|
11
|
+
# `NameError` before the call is even attempted, so the safe navigation
|
|
12
|
+
# operator is always redundant. Use the plain `.` operator instead.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# Model&.find(id)
|
|
17
|
+
# ENTITY_TYPES&.each { |type| type.to_s }
|
|
18
|
+
# Foo::Bar&.call
|
|
19
|
+
# ::Foo&.call
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# Model.find(id)
|
|
23
|
+
# ENTITY_TYPES.each { |type| type.to_s }
|
|
24
|
+
# Foo::Bar.call
|
|
25
|
+
# ::Foo.call
|
|
26
|
+
class ConstantSafety < Base
|
|
27
|
+
extend AutoCorrector
|
|
28
|
+
|
|
29
|
+
MSG = "Do not use safe navigation (`&.`) on a constant; constants are never `nil`, so use `.` instead."
|
|
30
|
+
|
|
31
|
+
def on_csend(node)
|
|
32
|
+
return unless node.receiver.const_type?
|
|
33
|
+
|
|
34
|
+
add_offense(node.loc.dot) do |corrector|
|
|
35
|
+
corrector.replace(node.loc.dot, ".")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Gusto
|
|
6
|
+
# Enforces that `FeatureFlag.active?` is called with a constant rather than a
|
|
7
|
+
# string literal. Defining flag keys as constants keeps them in one place,
|
|
8
|
+
# makes typos a load-time error instead of a silent always-off flag, and lets
|
|
9
|
+
# tools find every reference to a flag.
|
|
10
|
+
#
|
|
11
|
+
# `FeatureFlag` is a constant, so it is never nil and safe navigation
|
|
12
|
+
# (`FeatureFlag&.active?`) is never used; the cop only handles `on_send`.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# FeatureFlag.active?("some_feature_flag")
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# FeatureFlag.active?(SomeModule::SOME_FEATURE_FLAG)
|
|
20
|
+
class FeatureFlagConstants < Base
|
|
21
|
+
MSG = "FeatureFlag keys should be constants, not strings"
|
|
22
|
+
RESTRICT_ON_SEND = %i(active?).freeze
|
|
23
|
+
|
|
24
|
+
# @!method feature_flag_with_string?(node)
|
|
25
|
+
def_node_matcher :feature_flag_with_string?, <<~PATTERN
|
|
26
|
+
(send (const nil? :FeatureFlag) :active? (str _) ...)
|
|
27
|
+
PATTERN
|
|
28
|
+
|
|
29
|
+
def on_send(node)
|
|
30
|
+
add_offense(node) if feature_flag_with_string?(node)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Gusto
|
|
6
|
+
module Graphql
|
|
7
|
+
module IdDescriptionConcerns
|
|
8
|
+
# analytics_id is intentionally excluded
|
|
9
|
+
PRIMARY_ID_FIELDS = %i(id uuid).freeze
|
|
10
|
+
|
|
11
|
+
# Matches a GraphQL field declaration capturing name and type node.
|
|
12
|
+
FIELD_CALL_PATTERN = RuboCop::AST::NodePattern.new(<<~PATTERN)
|
|
13
|
+
(send nil? :field (sym $_name) $_ ...)
|
|
14
|
+
PATTERN
|
|
15
|
+
|
|
16
|
+
# Matches a GraphQL argument declaration capturing name and type node.
|
|
17
|
+
ARGUMENT_CALL_PATTERN = RuboCop::AST::NodePattern.new(<<~PATTERN)
|
|
18
|
+
(send nil? :argument (sym $_name) $_ ...)
|
|
19
|
+
PATTERN
|
|
20
|
+
|
|
21
|
+
# Mutation / input-object base classes conventionally end in one of these final
|
|
22
|
+
# segments (e.g. `Base::PermissionedMutation`, `Gusto::GraphQL::Objects::BaseInputObject`,
|
|
23
|
+
# `Base::Input`). We match on the base class's final name segment rather than a substring
|
|
24
|
+
# of its source, so unrelated bases that merely *contain* the word (e.g.
|
|
25
|
+
# `GraphQLMutationTool`, `MutationHelper`) are not misclassified.
|
|
26
|
+
MUTATION_OR_INPUT_BASE_SUFFIXES = %w(Mutation InputObject Input).freeze
|
|
27
|
+
|
|
28
|
+
def field_call?(node, &block)
|
|
29
|
+
FIELD_CALL_PATTERN.match(node, &block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def argument_call?(node, &block)
|
|
33
|
+
ARGUMENT_CALL_PATTERN.match(node, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def id_type?(type_node)
|
|
39
|
+
return false unless type_node
|
|
40
|
+
return false unless type_node.const_type?
|
|
41
|
+
|
|
42
|
+
const_name = type_node.const_name
|
|
43
|
+
const_name == "ID" || const_name == "GraphQL::Types::ID"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def array_id_type?(type_node)
|
|
47
|
+
return false unless type_node
|
|
48
|
+
|
|
49
|
+
# Match: [ID] or [GraphQL::Types::ID]
|
|
50
|
+
if type_node.array_type?
|
|
51
|
+
inner = type_node.children.first
|
|
52
|
+
return id_type?(inner)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_description(node)
|
|
59
|
+
description_from_positional_argument(node) ||
|
|
60
|
+
description_from_keyword_argument(node) ||
|
|
61
|
+
description_from_block(node)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def description_from_positional_argument(node)
|
|
65
|
+
node.arguments.find(&:str_type?)&.value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def description_from_keyword_argument(node)
|
|
69
|
+
node.each_child_node(:hash) do |arg|
|
|
70
|
+
arg.each_pair do |key, value|
|
|
71
|
+
return value.value if key.sym_type? && key.value == :description && value.str_type?
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def description_from_block(node)
|
|
79
|
+
parent = node.parent
|
|
80
|
+
return nil unless parent&.block_type? && parent.send_node == node
|
|
81
|
+
|
|
82
|
+
parent.body&.each_node(:send) do |send_node|
|
|
83
|
+
if send_node.method?(:description) && send_node.first_argument&.str_type?
|
|
84
|
+
return send_node.first_argument.value
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Find the GraphQL type name for the enclosing class or module
|
|
92
|
+
# Prefers graphql_name declaration over Ruby class/module name
|
|
93
|
+
# Returns nil if the name cannot be determined
|
|
94
|
+
def find_graphql_type_name(node)
|
|
95
|
+
# Try class first (most common), then module (for interfaces)
|
|
96
|
+
type_node = node.each_ancestor(:class).first || node.each_ancestor(:module).first
|
|
97
|
+
return nil unless type_node
|
|
98
|
+
|
|
99
|
+
# Look for graphql_name declaration in the type body
|
|
100
|
+
graphql_name = find_graphql_name_declaration(type_node)
|
|
101
|
+
return graphql_name if graphql_name
|
|
102
|
+
|
|
103
|
+
# Fall back to Ruby class/module name, stripping common suffixes
|
|
104
|
+
# GraphQL-ruby automatically strips "Type" from class names
|
|
105
|
+
# Interfaces conventionally use "Interface" suffix
|
|
106
|
+
# Also strip module namespacing, only use the final name
|
|
107
|
+
const_node = type_node.children.first
|
|
108
|
+
return nil unless const_node
|
|
109
|
+
|
|
110
|
+
full_name = const_node.const_name
|
|
111
|
+
type_name = full_name.split("::").last
|
|
112
|
+
strip_graphql_suffix(type_name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def strip_graphql_suffix(name)
|
|
116
|
+
name.chomp("Type").chomp("Interface")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def find_graphql_name_declaration(type_node)
|
|
120
|
+
type_body = type_node.body
|
|
121
|
+
return nil unless type_body
|
|
122
|
+
|
|
123
|
+
nodes = type_body_nodes(type_body)
|
|
124
|
+
graphql_name_call = find_graphql_name_call(nodes)
|
|
125
|
+
return nil unless graphql_name_call
|
|
126
|
+
|
|
127
|
+
first_arg = graphql_name_call.first_argument
|
|
128
|
+
first_arg.value if first_arg&.str_type?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def type_body_nodes(type_body)
|
|
132
|
+
if type_body.begin_type?
|
|
133
|
+
type_body.children
|
|
134
|
+
else
|
|
135
|
+
[type_body]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def find_graphql_name_call(nodes)
|
|
140
|
+
nodes.find { |node| node.send_type? && node.method?(:graphql_name) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def in_mutation_or_input_object?(node)
|
|
144
|
+
node.each_ancestor(:class).any? do |class_node|
|
|
145
|
+
base = class_node.parent_class
|
|
146
|
+
next false unless base&.const_type?
|
|
147
|
+
|
|
148
|
+
base.short_name.to_s.end_with?(*MUTATION_OR_INPUT_BASE_SUFFIXES)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "id_description_concerns"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Gusto
|
|
8
|
+
module Graphql
|
|
9
|
+
# Ensures ID type arguments on query fields follow the standardized description template:
|
|
10
|
+
# "The identifier of the <ObjectName> (`<ObjectName.fieldName>`) to filter by"
|
|
11
|
+
# "The identifiers of each <ObjectName> (`<ObjectName.fieldName>`) to filter by" (for arrays)
|
|
12
|
+
#
|
|
13
|
+
# This cop only applies to arguments on regular GraphQL objects/queries,
|
|
14
|
+
# NOT on mutations or input objects (see IdInputArgumentDescription for those).
|
|
15
|
+
#
|
|
16
|
+
# @example Bad
|
|
17
|
+
# argument :employee_id, ID, 'Employee ID', required: true
|
|
18
|
+
#
|
|
19
|
+
# @example Good
|
|
20
|
+
# argument :employee_id, ID, 'The identifier of the Employee (`Employee.id`) to filter by', required: true
|
|
21
|
+
# argument :employee_ids, [ID], 'The identifiers of each Employee (`Employee.id`) to filter by', required: true
|
|
22
|
+
#
|
|
23
|
+
class IdFieldArgumentDescription < Base
|
|
24
|
+
include IdDescriptionConcerns
|
|
25
|
+
|
|
26
|
+
MSG = "ID argument description should match template: " \
|
|
27
|
+
"'The identifier of the <ObjectName> (`<ObjectName.fieldName>`) to filter by'."
|
|
28
|
+
|
|
29
|
+
RESTRICT_ON_SEND = %i(argument).freeze
|
|
30
|
+
|
|
31
|
+
# A single "<ObjectName> (`<ObjectName.fieldName>`)" reference. Multiple may be joined
|
|
32
|
+
# with " or " for arguments that accept an id from more than one entity.
|
|
33
|
+
TYPE_REF = /\w+ \(`\w+\.\w+`\)/
|
|
34
|
+
|
|
35
|
+
# Single: "The identifier of the <ObjectName> (`<ObjectName.fieldName>`) to filter by"
|
|
36
|
+
SINGLE_PATTERN = /\AThe identifier of the #{TYPE_REF}(?: or #{TYPE_REF})* to filter by/
|
|
37
|
+
|
|
38
|
+
# Array: "The identifiers of each <ObjectName> (`<ObjectName.fieldName>`) to filter by"
|
|
39
|
+
ARRAY_PATTERN = /\AThe identifiers of each #{TYPE_REF}(?: or #{TYPE_REF})* to filter by/
|
|
40
|
+
|
|
41
|
+
def on_send(node)
|
|
42
|
+
# Skip if we're in a mutation or input object context because there's another cop for that
|
|
43
|
+
return if in_mutation_or_input_object?(node)
|
|
44
|
+
|
|
45
|
+
argument_call?(node) do |_name, type_node|
|
|
46
|
+
is_array = array_id_type?(type_node)
|
|
47
|
+
return unless id_type?(type_node) || is_array
|
|
48
|
+
|
|
49
|
+
desc = extract_description(node)
|
|
50
|
+
return if desc.nil? # No description - let other cops handle missing descriptions
|
|
51
|
+
return if valid_argument_description?(desc, is_array)
|
|
52
|
+
|
|
53
|
+
add_offense(node)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def valid_argument_description?(desc, is_array)
|
|
60
|
+
# Only validate the pattern format, not the object name.
|
|
61
|
+
# The object name should reference the actual GraphQL type,
|
|
62
|
+
# which may differ from what we'd infer from the argument name.
|
|
63
|
+
# e.g., argument :device_id could reference TrustedDevice type
|
|
64
|
+
pattern = is_array ? ARRAY_PATTERN : SINGLE_PATTERN
|
|
65
|
+
pattern.match?(desc)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|