rubocop-dev_doc 0.16.0 → 0.18.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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +312 -60
  3. data/config/obsoletion.yml +7 -3
  4. data/lib/dev_doc/i18n/tasks.rb +68 -0
  5. data/lib/dev_doc/railtie.rb +19 -0
  6. data/lib/dev_doc/test/lints/cross_tenant_canary_sweep.rb +2 -0
  7. data/lib/dev_doc/test/lints/domain_class_base.rb +138 -0
  8. data/lib/dev_doc/test/lints/http_driven_controller_tests.rb +2 -0
  9. data/lib/dev_doc/test/pseudo_i18n_crawler.rb +5 -0
  10. data/lib/rubocop/cop/dev_doc/i18n/hardcoded_text.rb +243 -0
  11. data/lib/rubocop/cop/dev_doc/i18n/localizable_props.rb +69 -19
  12. data/lib/rubocop/cop/dev_doc/i18n/require_translation.rb +13 -17
  13. data/lib/rubocop/cop/dev_doc/i18n/text_shape.rb +94 -0
  14. data/lib/rubocop/cop/dev_doc/rails/application_model_base.rb +56 -0
  15. data/lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb +81 -26
  16. data/lib/rubocop/cop/dev_doc/rails/no_errors_add_in_controller.rb +177 -0
  17. data/lib/rubocop/cop/dev_doc/rails/no_form_param_lookup_in_controller.rb +236 -0
  18. data/lib/rubocop/cop/dev_doc/style/in_memory_association_find.rb +162 -0
  19. data/lib/rubocop/cop/dev_doc/style/no_silent_rescue.rb +269 -0
  20. data/lib/rubocop/cop/dev_doc/style/prefer_allowlist_symbol_param.rb +193 -0
  21. data/lib/rubocop/cop/dev_doc/style/use_g_log.rb +133 -0
  22. data/lib/rubocop/cop/dev_doc/test/prefer_find_sole_by.rb +146 -0
  23. data/lib/rubocop/cop/dev_doc/test/test_name_special_characters.rb +108 -0
  24. data/lib/rubocop/cop/dev_doc/view/prefer_prop_over_name.rb +11 -1
  25. data/lib/rubocop/dev_doc/version.rb +1 -1
  26. metadata +16 -3
  27. data/lib/rubocop/cop/dev_doc/i18n/report_text.rb +0 -76
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05e1aabe21621ec2ef5867764976fcfb58d7e0bb42b6224f56065ac17bb5cd8b
4
- data.tar.gz: 2adbd5114fa685aaf7240c04df9fadb955d2cba1a0ccfd83d1e8fb44751f9743
3
+ metadata.gz: 3852833893968d60a5c0c198ffd3a2f34cb524a71998464a27803de2d56b44de
4
+ data.tar.gz: 1b4f675aa6967bca65d6f706997c06c73fe3b954b4092f6f8586e5c17b1e16b8
5
5
  SHA512:
6
- metadata.gz: ffe0a1d90cdf8d0a4dcee8ba865631f9d9c8e3a1e2b3de6af25aa6eb9441a966d715d099e3824e40ec5ec2640c001d9043f375e81fc590732309554b81b80e18
7
- data.tar.gz: 1d164d6cc051b8f216384ef334d0b5015a08d52a71cdb9b93e0cf065b6440e9a7285c3d4904d83be92936631b638428ff1dd816f39ace4a76b6053ee98b184b4
6
+ metadata.gz: 3b4be761462f9974fa8caa317e1df9d71ead56160dc4d9d0e7b98407af11a8835f09a888e4b8677448981c6e8e032129312d6e469f7134426714351e594cdc64
7
+ data.tar.gz: 5aca39f093385aae489a8c1a18fcf43eff4995dd7a5d575d237e5a472a8477457789a9cb201ce5ba09abdc6c49fcbf80fbd761467ee2d46447f5ee943111292f
data/config/default.yml CHANGED
@@ -199,6 +199,29 @@ DevDoc/Rails/NoCollectionIdsWriterInController:
199
199
  Include:
200
200
  - "app/controllers/**/*.rb"
201
201
 
202
+ # Route params are resource identity (a bad URL SHOULD 404), but a
203
+ # form-submitted attribute fed to a finder turns an expected form state
204
+ # (nothing picked yet) into RecordNotFound, and drags the validity check
205
+ # into the controller. The remedy is assignment: put the id on the model
206
+ # and let its validations render the failure.
207
+ DevDoc/Rails/NoFormParamLookupInController:
208
+ Description: "A record finder in a controller must not be fed by a form-submitted attribute (`params[:form][:attribute]`, `params.dig(:form, :attribute)`); assign the id to the model and let its validations decide."
209
+ Enabled: true
210
+ Include:
211
+ - "app/controllers/**/*.rb"
212
+
213
+ # The controller is the assignment bridge, not the validity judge: a
214
+ # controller-side `errors.add` is a validation living in the wrong layer,
215
+ # guarded once for one POST while every other write path skips it. The
216
+ # three delivery shapes (rescue, policy guard, verification bang) are
217
+ # auto-exempted — there the check already lives in the right layer and the
218
+ # line only renders its outcome.
219
+ DevDoc/Rails/NoErrorsAddInController:
220
+ Description: "Controllers must not write errors onto a model (`record.errors.add`); the rule belongs in a model validation or form-object PORO. Delivery of an outcome from a rescue, policy, or verification method is auto-exempted."
221
+ Enabled: true
222
+ Include:
223
+ - "app/controllers/**/*.rb"
224
+
202
225
  DevDoc/Rails/EnumMustBeSymbolized:
203
226
  Description: "Declare enums with `enum_symbolize :foo, { … }` instead of a bare `enum`, so the attribute type is set before the enum and the reader returns a symbol."
204
227
  Enabled: true
@@ -240,6 +263,14 @@ DevDoc/Style/AvoidInsecureSend:
240
263
  Description: "Avoid insecure dynamic `send`/`public_send` — `send` is always flagged; `public_send` requires a prefix or trusted source (validator/mailer)."
241
264
  Enabled: true
242
265
 
266
+ DevDoc/Style/PreferAllowlistSymbolParam:
267
+ Description: "Flag a hand-rolled `find`/`detect` allow-list lookup against params/request.headers; use `glib_allowlist_symbol_param` instead."
268
+ Enabled: true
269
+
270
+ DevDoc/Style/InMemoryAssociationFind:
271
+ Description: "Flag block-form `find`/`detect` on an association declared in the same file; use `find_by` unless the in-memory view is required, or disable with a reason."
272
+ Enabled: true
273
+
243
274
  DevDoc/Style/PreferPublicSend:
244
275
  Description: "Prefer `public_send` over `send` — visibility-respecting dispatch surfaces typos/misconfig and documents intent."
245
276
  Enabled: true
@@ -403,6 +434,18 @@ DevDoc/Rails/ApplicationRecordTransaction:
403
434
  Exclude:
404
435
  - "app/models/**/*.rb"
405
436
 
437
+ DevDoc/Rails/ApplicationModelBase:
438
+ Description: "Inherit the app's ApplicationModel root instead of including ActiveModel::Model directly (three-way rule, backend/03_model.md item 9)."
439
+ # Disabled by default: requires the project to have an ApplicationModel root
440
+ # over glib-web's Glib::Model. Enable per project, scoped to app/models; the
441
+ # root itself and any engine-side base need excluding where they legitimately
442
+ # carry the include.
443
+ Enabled: false
444
+ Include:
445
+ - "app/models/**/*.rb"
446
+ Exclude:
447
+ - "app/models/application_model.rb"
448
+
406
449
  DevDoc/Style/AvoidOptionsHash:
407
450
  Description: "Use keyword arguments instead of `**options` — typos raise `ArgumentError`; options hashes swallow them silently."
408
451
  Enabled: true
@@ -593,6 +636,29 @@ Style/SafeNavigationChainLength:
593
636
  Enabled: true
594
637
  Max: 1
595
638
 
639
+ # Early returns over nesting. Core default is already on; listed here because
640
+ # best_practices/backend/en/01a_defensive_programming.md #2 relies on it (the
641
+ # doc dropped its nested-vs-early-return example in favour of this cop and
642
+ # keeps only the guard-SHAPE guidance the cop cannot judge).
643
+ Style/GuardClause:
644
+ Enabled: true
645
+
646
+ # Core default is `MinNameLength: 1`, which never fires. Raised to 3 so a bare
647
+ # `|e|` / `|u|` / `|s|` block parameter is an offense — the reader should not
648
+ # have to infer the type from the receiver (see
649
+ # best_practices/intro/en/01_principle.md, Code Intent). The core AllowedNames
650
+ # for this cop is empty (the `at/by/id/…` list belongs to
651
+ # Naming/MethodParameterName); the one exemption here is `t`, the
652
+ # `create_table do |t|` idiom, which made up half of the hits when this was
653
+ # measured against a consuming app (2026-09-09: 87 offenses, 44 of them `|t|`
654
+ # in db/; the other 43 were real). AllowNamesEndingInNumbers stays at its core
655
+ # default (true), so `emp1` passes; `e1` still fails on length.
656
+ Naming/BlockParameterName:
657
+ Enabled: true
658
+ MinNameLength: 3
659
+ AllowedNames:
660
+ - t
661
+
596
662
  # `has_X?` reads more naturally than the cop's preferred `X?` in many cases —
597
663
  # `has_active_subscription?` vs `active_subscription?`, `has_assignee?(member)`
598
664
  # vs `assignee?(member)`. Disabled cross-project so individual `.rubocop.yml`
@@ -684,6 +750,40 @@ DevDoc/Test/NoPersistenceInMailerPreviews:
684
750
  - "spec/mailers/previews/**/*.rb"
685
751
  - "lib/mailer_previews/**/*.rb"
686
752
 
753
+ # A test name containing shell- or regex-significant characters breaks running
754
+ # that single test by name: unquoted `-n name` is mis-parsed by the shell
755
+ # (syntax error, redirect, expansion, substitution), and in the `/name/` regex
756
+ # filter form the regex-significant ones match nothing, so the runner reports
757
+ # 0 runs — which reads as a pass. Every character in the default set has a
758
+ # verified failure in at least one of the two forms (checked against a real
759
+ # minitest single-test run); `? { } / !` are deliberately NOT forbidden — see
760
+ # the cop docstring for their caveats. ForbiddenCharacters must list the same
761
+ # characters, in the same order, as the cop's DEFAULT_FORBIDDEN_CHARACTERS
762
+ # (specs enforce set parity; order keeps runtime messages matching specs).
763
+ DevDoc/Test/TestNameSpecialCharacters:
764
+ Description: "Test names must not contain characters that break single-test runs (shell mis-parse unquoted; `/name/` regex form runs 0 tests)."
765
+ Enabled: true
766
+ ForbiddenCharacters: "()[]*+^$\\&;|<>'\"`"
767
+ Include:
768
+ - "test/**/*.rb"
769
+ - "spec/**/*.rb"
770
+
771
+ # A bare-class `find_by` in a test returns whichever matching row the database
772
+ # yields first. When the attribute is not unique across the table (an email in
773
+ # a tenant-scoped membership table, a slug that repeats per country) the test
774
+ # binds to a different fixture per seed and fails far from the cause — the
775
+ # observed case reproduced only under one seed and read as a flake.
776
+ # `find_sole_by` raises unless exactly one row matches, turning the assumption
777
+ # into an assertion. `find_by!` autocorrects; `find_by` (nil-returning) does
778
+ # not, because a test may assert the nil — rewrite those to `exists?`.
779
+ # Requires Rails 7.0+ (`find_sole_by`); disable on older consumers.
780
+ DevDoc/Test/PreferFindSoleBy:
781
+ Description: "In tests, `Model.find_by`/`find_by!` on a bare class must be `find_sole_by` — an unscoped lookup by a non-unique attribute silently picks a row per run. Primary-key lookups and association-scoped receivers are not flagged."
782
+ Enabled: true
783
+ Include:
784
+ - "test/**/*.rb"
785
+ - "spec/**/*.rb"
786
+
687
787
  DevDoc/Test/RequireUnitTestJustification:
688
788
  Description: "Every test file must live in a recognized directory; unit-test directories additionally require a justification header (marker phrase: 'deliberate exception')."
689
789
  Enabled: true
@@ -903,6 +1003,159 @@ DevDoc/I18n/AvoidTitleizeHumanize:
903
1003
  - "app/views/**/*.jbuilder"
904
1004
  - "app/views/**/*.rb"
905
1005
 
1006
+ DevDoc/I18n/HardcodedText:
1007
+ Description: "Warn on any string literal that reads like user-facing copy, in any position — not only glib text props. Use `t(...)`."
1008
+ # A review aid, not a clean lint: it judges strings by shape (prose vs
1009
+ # identifier) and position, so it cannot tell copy a user sees from prose
1010
+ # that never leaves the server. Off by default and `warning` severity; run
1011
+ # it during a localization pass alongside RequireTranslation, which is
1012
+ # precise for glib props but blind to ternary/`||` fallbacks, hashes built in
1013
+ # locals, controller/model/mailer strings and constants.
1014
+ Enabled: false
1015
+ Severity: warning
1016
+ # ExemptKeys: hash keys whose values are never copy (style, routing, model
1017
+ # metadata, i18n plumbing). Every value under them is skipped.
1018
+ ExemptKeys:
1019
+ - styleClass
1020
+ - styleClasses
1021
+ - style
1022
+ - class
1023
+ - id
1024
+ - targetId
1025
+ - name
1026
+ - url
1027
+ - href
1028
+ - src
1029
+ - path
1030
+ - partial
1031
+ - template
1032
+ - layout
1033
+ - controller
1034
+ - action
1035
+ - as
1036
+ - format
1037
+ - content_type
1038
+ - mime_type
1039
+ - accept
1040
+ - class_name
1041
+ - source_type
1042
+ - foreign_key
1043
+ - primary_key
1044
+ - table_name
1045
+ - inverse_of
1046
+ - scope
1047
+ - locale
1048
+ # ExemptMethods: string receivers and direct arguments of these are
1049
+ # patterns, SQL, lookup keys or formats rather than copy.
1050
+ ExemptMethods:
1051
+ - "where"
1052
+ - "rewhere"
1053
+ - "not"
1054
+ - "or"
1055
+ - "having"
1056
+ - "order"
1057
+ - "reorder"
1058
+ - "group"
1059
+ - "select"
1060
+ - "joins"
1061
+ - "left_joins"
1062
+ - "from"
1063
+ - "pluck"
1064
+ - "pick"
1065
+ - "find_by_sql"
1066
+ - "count_by_sql"
1067
+ - "exec_query"
1068
+ - "execute"
1069
+ - "sql"
1070
+ - "sanitize_sql"
1071
+ - "sanitize_sql_array"
1072
+ - "sanitize_sql_for_conditions"
1073
+ - "=="
1074
+ - "!="
1075
+ - "==="
1076
+ - "eql?"
1077
+ - "equal?"
1078
+ - "=~"
1079
+ - "!~"
1080
+ - "include?"
1081
+ - "in?"
1082
+ - "start_with?"
1083
+ - "end_with?"
1084
+ - "match?"
1085
+ - "match"
1086
+ - "casecmp"
1087
+ - "casecmp?"
1088
+ - "gsub"
1089
+ - "gsub!"
1090
+ - "sub"
1091
+ - "sub!"
1092
+ - "tr"
1093
+ - "tr_s"
1094
+ - "delete"
1095
+ - "delete_prefix"
1096
+ - "delete_suffix"
1097
+ - "split"
1098
+ - "squeeze"
1099
+ - "partition"
1100
+ - "rpartition"
1101
+ - "[]"
1102
+ - "[]="
1103
+ - "dig"
1104
+ - "fetch"
1105
+ - "key?"
1106
+ - "has_key?"
1107
+ - "slice"
1108
+ - "except"
1109
+ - "values_at"
1110
+ - "fetch_values"
1111
+ - "send"
1112
+ - "public_send"
1113
+ - "__send__"
1114
+ - "respond_to?"
1115
+ - "const_get"
1116
+ - "const_defined?"
1117
+ - "instance_variable_get"
1118
+ - "instance_variable_set"
1119
+ - "attribute"
1120
+ - "require"
1121
+ - "require_relative"
1122
+ - "load"
1123
+ - "autoload"
1124
+ - "strftime"
1125
+ - "strptime"
1126
+ - "parse"
1127
+ - "header"
1128
+ - "set_header"
1129
+ - "add_header"
1130
+ - "image_url"
1131
+ - "image_path"
1132
+ - "image_tag"
1133
+ - "asset_url"
1134
+ - "asset_path"
1135
+ # DeveloperFacingMethods: receiver-less calls whose whole subtree is
1136
+ # developer-facing (exception and console text). `logger.*` calls are
1137
+ # always skipped.
1138
+ DeveloperFacingMethods:
1139
+ - raise
1140
+ - fail
1141
+ - warn
1142
+ - abort
1143
+ - puts
1144
+ - print
1145
+ - p
1146
+ - pp
1147
+ # DeveloperFacingReceivers: constants whose every call reports to developers
1148
+ # (error trackers), so string arguments to them are never copy.
1149
+ DeveloperFacingReceivers:
1150
+ - Rollbar
1151
+ - Sentry
1152
+ - Honeybadger
1153
+ - Bugsnag
1154
+ - Airbrake
1155
+ Include:
1156
+ - "app/**/*.rb"
1157
+ - "app/**/*.jbuilder"
1158
+
906
1159
  DevDoc/View/PreferPropOverName:
907
1160
  Description: "Bind form fields to model attributes with `prop:` (i18n-resolved label/placeholder) instead of a raw `name: 'model[attr]'` with hardcoded text."
908
1161
  # Disabled by default (new department) — enable per consumer .rubocop.yml.
@@ -917,10 +1170,17 @@ DevDoc/View/PreferPropOverName:
917
1170
  # credit-card fields capture transient credentials that must never bind to
918
1171
  # a persisted model, and dynamic-group names are array-param structural keys
919
1172
  # with no label/placeholder to derive.
1173
+ # fields_file (deprecated field class) posts a raw upload the controller
1174
+ # often parses and discards (CSV imports, server-side processing) — its
1175
+ # prop: machinery calls `.attached?` on the bound model value, so a
1176
+ # conversion of a non-attachment param raises at render and the unsafe
1177
+ # autocorrect would produce exactly that. Attachment-backed uploads belong
1178
+ # on fields_upload, which binds prop: fine.
920
1179
  ExemptFieldMethods:
921
1180
  - fields_hidden
922
1181
  - fields_creditCard
923
1182
  - fields_dynamicGroup
1183
+ - fields_file
924
1184
  Include:
925
1185
  - "app/views/**/*.jbuilder"
926
1186
  - "app/views/**/*.rb"
@@ -949,17 +1209,35 @@ DevDoc/I18n/RequireTranslation:
949
1209
  - shareButton
950
1210
  - fields_text
951
1211
  - fields_number
952
- - fields_select
1212
+ - fields_email
1213
+ - fields_url
953
1214
  - fields_password
1215
+ - fields_timer
1216
+ - fields_submit
954
1217
  - fields_textarea
1218
+ - fields_richText
1219
+ - fields_select
1220
+ - fields_dynamicSelect
1221
+ - fields_dynamicGroup
955
1222
  - fields_check
956
1223
  - fields_checkGroup
957
1224
  - fields_chipGroup
958
- - fields_timeZone
1225
+ - fields_radio
959
1226
  - fields_radioGroup
1227
+ - fields_timeZone
960
1228
  - fields_date
961
1229
  - fields_datetime
1230
+ - fields_file
962
1231
  - fields_upload
1232
+ - fields_sign
1233
+ - fields_location
1234
+ - fields_phone
1235
+ - fields_otp
1236
+ - fields_captcha
1237
+ - fields_rating
1238
+ - fields_creditCard
1239
+ - fields_stripeToken
1240
+ - fields_stripeExternalAccount
963
1241
  - dialogs_alert
964
1242
  - dialogs_notification
965
1243
  - snackbars_alert
@@ -972,6 +1250,7 @@ DevDoc/I18n/RequireTranslation:
972
1250
  - subsubtitle
973
1251
  - label
974
1252
  - placeholder
1253
+ - hint
975
1254
  - text
976
1255
  - message
977
1256
  - description
@@ -1009,62 +1288,35 @@ DevDoc/Rails/NoManualRecordInvalid:
1009
1288
  Include:
1010
1289
  - "app/models/**/*.rb"
1011
1290
 
1012
- DevDoc/I18n/ReportText:
1013
- Description: "Report every user-facing glib text prop — hardcoded and already-localized — to collect all possible texts."
1014
- # A tooling aid, not a lint: unlike RequireTranslation it fires on *every*
1015
- # text value (including `t(...)` calls) so you can sweep the codebase and
1016
- # collect the full list of user-facing strings. Off by default and `info`
1017
- # severity. Mirrors RequireTranslation's WatchedMethods/LocalizableKeys.
1018
- Enabled: false
1019
- Severity: info
1020
- WatchedMethods:
1021
- - h1
1022
- - h2
1023
- - h3
1024
- - h4
1025
- - h5
1026
- - h6
1027
- - p
1028
- - label
1029
- - markdown
1030
- - html
1031
- - button
1032
- - switch
1033
- - chip
1034
- - progressCircle
1035
- - shareButton
1036
- - fields_text
1037
- - fields_number
1038
- - fields_select
1039
- - fields_password
1040
- - fields_textarea
1041
- - fields_check
1042
- - fields_checkGroup
1043
- - fields_chipGroup
1044
- - fields_timeZone
1045
- - fields_radioGroup
1046
- - fields_date
1047
- - fields_datetime
1048
- - fields_upload
1049
- - dialogs_alert
1050
- - dialogs_notification
1051
- - snackbars_alert
1052
- - snackbars_select
1053
- - banners_alert
1054
- - banners_select
1055
- LocalizableKeys:
1056
- - title
1057
- - subtitle
1058
- - subsubtitle
1059
- - label
1060
- - placeholder
1061
- - text
1062
- - message
1063
- - description
1064
- - uploadText
1065
- - leftText
1066
- - rightText
1067
- - onLabel
1291
+ # Strict superset of Lint/SuppressedException at its default settings — disable
1292
+ # the stock cop when enabling this one (see the cop docstring). Modifier
1293
+ # rescues stay out of scope: Style/RescueModifier owns them.
1294
+ DevDoc/Style/NoSilentRescue:
1295
+ Description: "A rescue body must re-raise, report the failure (a logger/error-monitor call), or state a justification — never swallow it silently."
1296
+ Enabled: true
1068
1297
  Include:
1069
- - "app/views/**/*.jbuilder"
1070
- - "app/views/**/*.rb"
1298
+ - "app/**/*.rb"
1299
+ - "lib/**/*.rb"
1300
+ JustificationMarker: "silent-rescue:"
1301
+ ReportingMethods:
1302
+ - "log"
1303
+ - "warn"
1304
+ - "error"
1305
+ - "errors"
1306
+ - "exception"
1307
+ - "fatal"
1308
+ - "critical"
1309
+ - "notify"
1310
+ - "notice"
1311
+ - "report"
1312
+ - "capture"
1313
+ - "track"
1314
+ - "instrument"
1315
+ - "monitor"
1316
+ - "alert"
1317
+ - "breadcrumb"
1318
+ - "handle"
1319
+
1320
+ DevDoc/Style/UseGLog:
1321
+ Description: "Route logging through GLog (glib-web) — flag Rails.logger.<any> and Rollbar's reporting methods (error/warning/info/debug/critical/log) outside the facade; Rollbar.configure and other setup calls are not reporting."
1322
+ Enabled: true
@@ -1,5 +1,9 @@
1
- # Cop renames across rubocop-dev_doc versions. RuboCop surfaces these as
2
- # actionable "has been renamed" errors when a project's config or inline
3
- # directives use the old name.
1
+ # Cop renames and removals across rubocop-dev_doc versions. RuboCop surfaces
2
+ # these as actionable "has been renamed"/"has been removed" errors when a
3
+ # project's config or inline directives use the old name.
4
4
  renamed:
5
5
  DevDoc/Migration/AvoidBypassingValidation: DevDoc/Rails/AvoidBypassingValidation
6
+
7
+ removed:
8
+ DevDoc/I18n/ReportText:
9
+ alternatives: DevDoc/I18n/HardcodedText
@@ -0,0 +1,68 @@
1
+ # Rake tasks wrapping the commands in docs/localization.md, so a project runs
2
+ # `bin/rails dev_doc:i18n:scan` instead of copying long rubocop invocations.
3
+ #
4
+ # Loaded by DevDoc::Railtie (lib/dev_doc/railtie.rb) in projects that bundle
5
+ # the gem with `require: 'dev_doc/railtie'`.
6
+ #
7
+ # Tasks (paths default to `app`; pass several as `[app/views,app/mailers]`):
8
+ #
9
+ # bin/rails dev_doc:i18n:scan[paths] Recipe A — hardcoded text
10
+ # bin/rails dev_doc:i18n:rekey[paths] Recipe B — keys to move
11
+ #
12
+ # Each task accepts OUT=<file> to save the findings instead of printing
13
+ # them (Recipe C hands that file to an AI assistant), and exit with rubocop's
14
+ # status: 0 clean, 1 findings, 2 error.
15
+ require 'rake'
16
+ require 'fileutils'
17
+
18
+ module DevDoc
19
+ module I18n
20
+ # `dev_doc:i18n:*` rake tasks: the localization recipes' commands, runnable
21
+ # from a project that bundles the gem.
22
+ module Tasks
23
+ extend Rake::DSL
24
+
25
+ HARDCODED_COPS = %w[
26
+ DevDoc/I18n/RequireTranslation
27
+ DevDoc/I18n/HardcodedText
28
+ DevDoc/I18n/AvoidTitleizeHumanize
29
+ ].freeze
30
+ RUBOCOP_TASKS = {
31
+ scan: [HARDCODED_COPS, 'List hardcoded user-facing text (Recipe A)'],
32
+ rekey: [%w[DevDoc/I18n/TranslationKeyPrefix], 'List lazy keys and keys outside AllowedPrefixes (Recipe B)']
33
+ }.freeze
34
+ DEFAULT_PATHS = %w[app].freeze
35
+
36
+ def self.define
37
+ namespace :dev_doc do
38
+ namespace :i18n do
39
+ RUBOCOP_TASKS.each do |name, (cops, summary)|
40
+ desc "#{summary}; OUT=<file> saves the findings"
41
+ task(name) { |_t, args| run_rubocop(cops, args.to_a) }
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.rubocop_args(cops, paths, out: nil)
48
+ args = %w[--format clang]
49
+ # --out binds to the --format before it, so it must follow it directly.
50
+ args += ['--out', out] if out
51
+ args + ['--only', cops.join(','), *(paths.empty? ? DEFAULT_PATHS : paths)]
52
+ end
53
+
54
+ def self.run_rubocop(cops, paths)
55
+ require 'rubocop'
56
+
57
+ out = ENV.fetch('OUT', '')
58
+ out = nil if out.empty?
59
+ FileUtils.mkdir_p(File.dirname(out)) if out
60
+ status = RuboCop::CLI.new.run(rubocop_args(cops, paths, out: out))
61
+ puts "Findings written to #{out}" if out && status != 2
62
+ exit(status) unless status.zero?
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ DevDoc::I18n::Tasks.define
@@ -0,0 +1,19 @@
1
+ # Registers dev-doc's rake tasks in a Rails app. Projects name this file as the
2
+ # gem's require instead of `require: false`:
3
+ #
4
+ # group :development, :test do
5
+ # gem 'rubocop-dev_doc', require: 'dev_doc/railtie'
6
+ # end
7
+ #
8
+ # Booting the app loads only this Railtie; RuboCop itself is loaded when a task
9
+ # runs, so app boot stays as light as with `require: false`.
10
+ require 'rails/railtie'
11
+
12
+ module DevDoc
13
+ # Loads the `dev_doc:*` rake tasks when Rails loads the app's tasks.
14
+ class Railtie < Rails::Railtie
15
+ rake_tasks do
16
+ require 'dev_doc/i18n/tasks'
17
+ end
18
+ end
19
+ end
@@ -77,6 +77,8 @@ module DevDoc
77
77
  def __dev_doc_response_body
78
78
  response&.body.to_s
79
79
  rescue StandardError
80
+ # silent-rescue: the canary check must never fail on an unreadable
81
+ # response body; an empty body simply carries no markers to match.
80
82
  ''
81
83
  end
82
84