rubocop-dev_doc 0.10.2 → 0.10.3
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ba7422e97cc011bf4700f5f6bfd1c87fd7e358c9c707baf73b5360abbf40129
|
|
4
|
+
data.tar.gz: 10ced56ec89a8d05f3897c76ff85df357cb52a9dc97d155f7caf11b1917faaa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 403c116d677b765d86ef3a8852b1087ffa5821a12a1db76f59343d067cfd6c942c4fe1ae69c65a67ae1ad24a07922e7312f2a4f2a84b5164e379f8928c90a57c
|
|
7
|
+
data.tar.gz: 4bd0b0fb718cf6ac10540735e49077da57e0d34b54111dcc5fe9da0a531383546eb83a9300503a10612f7bb0f90b6f6434acf0b307ea6125316202563bd89a52
|
data/config/default.yml
CHANGED
|
@@ -170,7 +170,7 @@ DevDoc/Rails/NoPerformLaterInModel:
|
|
|
170
170
|
- "app/models/**/*.rb"
|
|
171
171
|
|
|
172
172
|
DevDoc/Rails/EnumMustBeSymbolized:
|
|
173
|
-
Description: "
|
|
173
|
+
Description: "Pair every `enum :foo` with `enum_symbolize :foo` so the reader returns a symbol."
|
|
174
174
|
Enabled: true
|
|
175
175
|
Include:
|
|
176
176
|
- "app/models/**/*.rb"
|
|
@@ -436,18 +436,6 @@ Rails/SaveBang:
|
|
|
436
436
|
Exclude:
|
|
437
437
|
- "app/controllers/**/*"
|
|
438
438
|
|
|
439
|
-
# MySQL-oriented performance cop: it wants multiple ALTER TABLE operations on
|
|
440
|
-
# one table combined into `change_table :t, bulk: true` to avoid repeated
|
|
441
|
-
# full-table rewrites. That payoff is real on MySQL/MariaDB but marginal on
|
|
442
|
-
# PostgreSQL, where ALTER sub-commands are cheap metadata ops — and our projects
|
|
443
|
-
# run PostgreSQL. It also contradicts our own doctrine: the `bulk: true` form it
|
|
444
|
-
# demands trips DevDoc/Migration/AvoidColumnDefault (on the backfill `default:`)
|
|
445
|
-
# and Rails/ReversibleMigration (on `t.change_default`), neither disable-able
|
|
446
|
-
# inline, for the standard add-column-then-drop-default backfill — making the
|
|
447
|
-
# three rules mutually unsatisfiable. Off for all projects.
|
|
448
|
-
Rails/BulkChangeTable:
|
|
449
|
-
Enabled: false
|
|
450
|
-
|
|
451
439
|
# Superseded by DevDoc/Style/CaseElseDecision below: same detection, but the
|
|
452
440
|
# offense message carries the raise/report/fall-through decision framework
|
|
453
441
|
# instead of upstream's "Missing else statement" (which teaches the wrong
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Pseudo-localization backend — a hardcoded-text detector.
|
|
2
|
+
#
|
|
3
|
+
# When installed (see PseudoLocale.install!), an extra locale `en-PSEUDO`
|
|
4
|
+
# becomes available. Every string resolved through Rails `t()` is returned as
|
|
5
|
+
# its real English value, accented and wrapped in `⟦ ⟧` markers, e.g.
|
|
6
|
+
#
|
|
7
|
+
# t('energies.show.title') # "Monthly Statement"
|
|
8
|
+
# --> rendered under en-PSEUDO --> "⟦Móñţĥļý Šţáţéméñţ⟧"
|
|
9
|
+
#
|
|
10
|
+
# Render any page under the pseudo locale and scan the output: any user-facing
|
|
11
|
+
# text WITHOUT the `⟦ ⟧` markers never went through `t()` and is therefore a
|
|
12
|
+
# hardcoded-string candidate — regardless of whether it came from a view,
|
|
13
|
+
# helper, concern, module, or service object.
|
|
14
|
+
#
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Per-project integration
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# This gem is a `:development, :test`-only dependency, so it is NOT loaded in
|
|
19
|
+
# production and this file is only required when detection mode is on. The
|
|
20
|
+
# project initializer requires it and installs the backend under PSEUDO_I18N=1:
|
|
21
|
+
#
|
|
22
|
+
# # config/initializers/pseudo_locale.rb
|
|
23
|
+
# if ENV['PSEUDO_I18N'] == '1'
|
|
24
|
+
# require 'dev_doc/i18n/pseudo_locale'
|
|
25
|
+
# DevDoc::I18n::PseudoLocale.install!
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# `mark` (below) is called from production code paths to flag externally-managed
|
|
29
|
+
# strings for the scanner. Because this gem is absent in production, call it
|
|
30
|
+
# through a null-guard so it degrades to the identity when the gem is not
|
|
31
|
+
# loaded — the value is untouched anyway when detection mode is off:
|
|
32
|
+
#
|
|
33
|
+
# def pseudo_mark(value)
|
|
34
|
+
# return value unless defined?(DevDoc::I18n::PseudoLocale)
|
|
35
|
+
# DevDoc::I18n::PseudoLocale.mark(value)
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# Guarded behind PSEUDO_I18N, so normal dev/prod is untouched and the fake
|
|
39
|
+
# locale can never leak to real users.
|
|
40
|
+
module DevDoc
|
|
41
|
+
module I18n
|
|
42
|
+
module PseudoLocale
|
|
43
|
+
PSEUDO_LOCALE = 'en-PSEUDO'
|
|
44
|
+
MARKER = '⟦'
|
|
45
|
+
|
|
46
|
+
def self.active?
|
|
47
|
+
ENV['PSEUDO_I18N'] == '1'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Marks an externally-managed string — CMS content, assessment-YAML
|
|
51
|
+
# values, or a chosen ActiveRecord display value — so the hardcoded-text
|
|
52
|
+
# scanner treats it as already-handled and skips it. Returns the string
|
|
53
|
+
# untouched when detection mode is off, so it is safe on production paths
|
|
54
|
+
# (call it through a null-guard; see the header comment).
|
|
55
|
+
def self.mark(value)
|
|
56
|
+
return value unless active?
|
|
57
|
+
return value unless value.is_a?(String) && !value.empty?
|
|
58
|
+
|
|
59
|
+
value.include?(MARKER) ? value : "#{MARKER}#{value}⟧"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Letter map — readable but unmistakably "transformed".
|
|
63
|
+
MAP = {
|
|
64
|
+
'a' => 'á', 'b' => 'ƀ', 'c' => 'ç', 'd' => 'ð', 'e' => 'é', 'f' => 'ƒ',
|
|
65
|
+
'g' => 'ĝ', 'h' => 'ĥ', 'i' => 'í', 'j' => 'ĵ', 'k' => 'ķ', 'l' => 'ļ',
|
|
66
|
+
'm' => 'ɱ', 'n' => 'ñ', 'o' => 'ö', 'p' => 'þ', 'q' => 'ɋ', 'r' => 'ŕ',
|
|
67
|
+
's' => 'š', 't' => 'ţ', 'u' => 'ü', 'v' => 'ṽ', 'w' => 'ŵ', 'x' => 'ӿ',
|
|
68
|
+
'y' => 'ý', 'z' => 'ž',
|
|
69
|
+
'A' => 'Á', 'B' => 'Ɓ', 'C' => 'Ç', 'D' => 'Ð', 'E' => 'É', 'F' => 'Ƒ',
|
|
70
|
+
'G' => 'Ĝ', 'H' => 'Ĥ', 'I' => 'Í', 'J' => 'Ĵ', 'K' => 'Ķ', 'L' => 'Ļ',
|
|
71
|
+
'M' => 'Ṁ', 'N' => 'Ñ', 'O' => 'Ö', 'P' => 'Þ', 'Q' => 'Ɋ', 'R' => 'Ŕ',
|
|
72
|
+
'S' => 'Š', 'T' => 'Ţ', 'U' => 'Ü', 'V' => 'Ṽ', 'W' => 'Ŵ', 'X' => 'Ӿ',
|
|
73
|
+
'Y' => 'Ý', 'Z' => 'Ž'
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# Wires the pseudo backend into I18n and forces every request to render in
|
|
77
|
+
# it. Idempotent — safe to call once from the project initializer.
|
|
78
|
+
def self.install!
|
|
79
|
+
::I18n::Backend::Simple.prepend(Backend)
|
|
80
|
+
::I18n.available_locales |= [PSEUDO_LOCALE.to_sym]
|
|
81
|
+
force_locale!
|
|
82
|
+
::Rails.logger.info('[pseudo_locale] en-PSEUDO locale enabled (forced) for hardcoded-text detection') if defined?(::Rails.logger)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Force every request to render in the pseudo locale.
|
|
86
|
+
#
|
|
87
|
+
# Without this, an app's `default_url_options` typically derives the URL
|
|
88
|
+
# locale via `I18n.locale.to_s.split('-')[0]` ("en-PSEUDO" -> "en"), so
|
|
89
|
+
# every link and redirect the app generates carries `locale=en`;
|
|
90
|
+
# `set_locale` then honours that param and renders plain English — the
|
|
91
|
+
# pseudo transform never fires on any navigated page. Overriding
|
|
92
|
+
# `set_locale` here pins the locale regardless of the incoming param,
|
|
93
|
+
# which is exactly what a "render everything pseudo" detection mode wants.
|
|
94
|
+
def self.force_locale!
|
|
95
|
+
::Rails.application.config.to_prepare do
|
|
96
|
+
ApplicationController.class_eval do
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def set_locale
|
|
100
|
+
::I18n.locale = DevDoc::I18n::PseudoLocale::PSEUDO_LOCALE.to_sym
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# I18n backend override: resolves the real English entry, then accents it.
|
|
107
|
+
module Backend
|
|
108
|
+
# Namespaces whose values are format directives / structural data, not
|
|
109
|
+
# copy. Accenting these would corrupt strftime (`%A`), number formats.
|
|
110
|
+
SKIP_NAMESPACES = %w[i18n date time datetime number support].freeze
|
|
111
|
+
|
|
112
|
+
def lookup(locale, key, scope = [], options = ::I18n::EMPTY_HASH)
|
|
113
|
+
return super unless locale.to_s == PSEUDO_LOCALE
|
|
114
|
+
|
|
115
|
+
# Resolve the real English entry (template, before interpolation).
|
|
116
|
+
real = super(:en, key, scope, options)
|
|
117
|
+
return real if real.nil? || skip?(key, scope)
|
|
118
|
+
|
|
119
|
+
pseudoize(real)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def skip?(key, scope)
|
|
125
|
+
head = Array(scope).first || key.to_s[/\A[^.]+/]
|
|
126
|
+
SKIP_NAMESPACES.include?(head.to_s)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Recurses so pluralization hashes ({ one:, other: }) and arrays work.
|
|
130
|
+
def pseudoize(value)
|
|
131
|
+
case value
|
|
132
|
+
when String then wrap(value)
|
|
133
|
+
when Hash then value.transform_values { |v| pseudoize(v) }
|
|
134
|
+
when Array then value.map { |v| pseudoize(v) }
|
|
135
|
+
else value
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Some t() values are not display copy — asset filenames, paths, URLs.
|
|
140
|
+
# The asset helpers feed these to Vite, which raises on a missing
|
|
141
|
+
# manifest entry, so accenting them 500s the page. Leave them untouched.
|
|
142
|
+
#
|
|
143
|
+
# Note: do NOT treat a bare "/" as a path — display copy legitimately
|
|
144
|
+
# contains slashes ("Lamp/Bulbs", "Food/Food Scraps"). A real path is an
|
|
145
|
+
# all-lowercase identifier chain (`shared/layouts/main`, `images/icon`);
|
|
146
|
+
# copy has capitals or spaces, so it won't match the lowercase-path rule.
|
|
147
|
+
NON_COPY = %r{
|
|
148
|
+
\A\s*\z # blank
|
|
149
|
+
| \Ahttps?:// # url
|
|
150
|
+
| \A[a-z0-9_]+(?:[/.][a-z0-9_]+)+\z # lowercase path / dotted id (shared/layouts/main)
|
|
151
|
+
}x
|
|
152
|
+
|
|
153
|
+
ASSET_EXT = /\.(?:png|jpe?g|svg|gif|webp|ico|bmp|css|js|mjs|html?|pdf|woff2?|ttf|otf|mp[34]|json|xml|csv)\z/i
|
|
154
|
+
|
|
155
|
+
def wrap(str)
|
|
156
|
+
return str if str.match?(NON_COPY) || str.match?(ASSET_EXT)
|
|
157
|
+
|
|
158
|
+
# Split on %{name} placeholders (captured), accent only the literal
|
|
159
|
+
# segments so interpolation variables and their data stay intact.
|
|
160
|
+
body = str.split(/(%\{[^}]+\})/).each_with_index.map do |part, i|
|
|
161
|
+
i.odd? ? part : part.chars.map { |c| MAP[c] || c }.join
|
|
162
|
+
end.join
|
|
163
|
+
"⟦#{body}⟧"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Test
|
|
5
|
+
# Pseudo-localization hardcoded-text crawler.
|
|
6
|
+
#
|
|
7
|
+
# Reuses the glib-web JSON-UI crawler (per-role login + fixtures + full
|
|
8
|
+
# reachability) but renders every page under the `en-PSEUDO` locale and
|
|
9
|
+
# reports any localizable text prop that comes back WITHOUT the `⟦ ⟧`
|
|
10
|
+
# marker — i.e. user-facing text that bypassed Rails `t()` and is therefore
|
|
11
|
+
# a hardcoded-string candidate, no matter which view/helper/concern/module/
|
|
12
|
+
# service emitted it. See dev_doc/i18n/pseudo_locale.rb.
|
|
13
|
+
#
|
|
14
|
+
# Unlike the golden-file JSON-UI crawler this does NOT assert the page log
|
|
15
|
+
# (the accented pseudo output would never match committed baselines); it
|
|
16
|
+
# drives the router directly and only collects unwrapped text.
|
|
17
|
+
#
|
|
18
|
+
# ------------------------------------------------------------------------
|
|
19
|
+
# Per-project integration
|
|
20
|
+
# ------------------------------------------------------------------------
|
|
21
|
+
# Include this module in a single integration test and supply the project's
|
|
22
|
+
# own login credentials as `test` cases. The host class must also include
|
|
23
|
+
# `Glib::TestHelpers` (for HOST / login / logout) and load fixtures. The
|
|
24
|
+
# pseudo backend must be installed — see dev_doc/i18n/pseudo_locale.rb.
|
|
25
|
+
#
|
|
26
|
+
# # test/integration/pseudo_i18n_crawler_test.rb
|
|
27
|
+
# require 'test_helper'
|
|
28
|
+
# require 'dev_doc/test/pseudo_i18n_crawler'
|
|
29
|
+
#
|
|
30
|
+
# class PseudoI18nCrawlerTest < ActionDispatch::IntegrationTest
|
|
31
|
+
# include Glib::TestHelpers
|
|
32
|
+
# include DevDoc::Test::PseudoI18nCrawler
|
|
33
|
+
#
|
|
34
|
+
# self.fixture_paths = ['test/fixtures']
|
|
35
|
+
# fixtures :all
|
|
36
|
+
#
|
|
37
|
+
# test 'crawl superadmin' do
|
|
38
|
+
# crawl_pseudo(email: 'super@admin.com', password: 'password', device: 'web', version: nil)
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# Run with the pseudo backend enabled:
|
|
43
|
+
#
|
|
44
|
+
# PSEUDO_I18N=1 bin/rails test test/integration/pseudo_i18n_crawler_test.rb
|
|
45
|
+
#
|
|
46
|
+
# Findings are printed and written to
|
|
47
|
+
# test/integration/pseudo_i18n_crawler_test_results/<email>.txt
|
|
48
|
+
module PseudoI18nCrawler
|
|
49
|
+
MARKER = '⟦'.freeze
|
|
50
|
+
|
|
51
|
+
# Mirrors the cop's DEFAULT_LOCALIZABLE_KEYS — keep in sync with
|
|
52
|
+
# lib/rubocop/cop/dev_doc/i18n/localizable_props.rb. The crawler can only
|
|
53
|
+
# flag hardcoded text on keys it walks, so any key the cop treats as
|
|
54
|
+
# localizable (message/description on dialogs & banners, uploadText on
|
|
55
|
+
# fields_upload, on/left/rightText on switch & toggles) must be listed.
|
|
56
|
+
TEXT_KEYS = %w[
|
|
57
|
+
title
|
|
58
|
+
subtitle
|
|
59
|
+
subsubtitle
|
|
60
|
+
label
|
|
61
|
+
placeholder
|
|
62
|
+
text
|
|
63
|
+
message
|
|
64
|
+
description
|
|
65
|
+
uploadText
|
|
66
|
+
leftText
|
|
67
|
+
rightText
|
|
68
|
+
onLabel
|
|
69
|
+
].to_set
|
|
70
|
+
|
|
71
|
+
# The test class that included the module — the prepended Scanner (a
|
|
72
|
+
# single module shared across the Http class) dispatches scan results
|
|
73
|
+
# back to it.
|
|
74
|
+
class << self
|
|
75
|
+
attr_accessor :host_class
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Scan every crawled JSON-UI response as it is fetched.
|
|
79
|
+
module Scanner
|
|
80
|
+
# Signature must mirror Glib::JsonCrawler::Http#fetch for the
|
|
81
|
+
# prepend/super override to work, so the boolean positional arg can't
|
|
82
|
+
# become a kwarg.
|
|
83
|
+
def fetch(method, url, action, params = {}, inspect_result = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
84
|
+
result = super
|
|
85
|
+
if method == :get
|
|
86
|
+
rec = instance_variable_get(:@content_history)&.last
|
|
87
|
+
PseudoI18nCrawler.host_class.scan_body(url, rec[:response]) if rec && rec[:url] == url
|
|
88
|
+
end
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.included(base)
|
|
94
|
+
base.extend(ClassMethods)
|
|
95
|
+
PseudoI18nCrawler.host_class = base
|
|
96
|
+
|
|
97
|
+
# Prepend the Scanner onto the crawler HTTP client exactly once, no
|
|
98
|
+
# matter how many test classes include the module.
|
|
99
|
+
unless Glib::JsonCrawler::Http.ancestors.include?(Scanner)
|
|
100
|
+
Glib::JsonCrawler::Http.prepend(Scanner)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
base.setup do
|
|
104
|
+
skip 'Set PSEUDO_I18N=1 to run the pseudo-localization scan.' unless ENV['PSEUDO_I18N'] == '1'
|
|
105
|
+
skip 'en-PSEUDO locale not loaded — is config/initializers/pseudo_locale.rb active?' unless ::I18n.available_locales.include?(:'en-PSEUDO')
|
|
106
|
+
|
|
107
|
+
self.class.hits = {}
|
|
108
|
+
@previous_default_locale = ::I18n.default_locale
|
|
109
|
+
# The app's set_locale falls back to I18n.default_locale when no
|
|
110
|
+
# `_lang` param is present (crawler URLs carry none), so this renders
|
|
111
|
+
# every page in pseudo without touching the crawler's URLs.
|
|
112
|
+
::I18n.default_locale = :'en-PSEUDO'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
base.teardown do
|
|
116
|
+
::I18n.default_locale = @previous_default_locale if @previous_default_locale
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
module ClassMethods
|
|
121
|
+
attr_accessor :hits # { url => [ "[key] value", ... ] }
|
|
122
|
+
|
|
123
|
+
# Directory of the fixtures scanned to filter out AR display values.
|
|
124
|
+
def fixture_dir
|
|
125
|
+
@fixture_dir ||= ::Rails.root.join('test/fixtures').to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Where per-role reports are written.
|
|
129
|
+
def report_dir
|
|
130
|
+
@report_dir ||= ::Rails.root.join('test/integration/pseudo_i18n_crawler_test_results').to_s
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def scan_body(url, body)
|
|
134
|
+
return unless body.is_a?(String) && body.lstrip.start_with?('{', '[')
|
|
135
|
+
|
|
136
|
+
json = begin
|
|
137
|
+
JSON.parse(body)
|
|
138
|
+
rescue JSON::ParserError
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
walk(json) do |key, value|
|
|
143
|
+
if value.include?(MARKER)
|
|
144
|
+
@wrapped = (@wrapped || 0) + 1
|
|
145
|
+
elsif trivial?(value)
|
|
146
|
+
next
|
|
147
|
+
elsif fixture_strings.include?(value.strip)
|
|
148
|
+
# ActiveRecord display value (record title/name/user copy) that
|
|
149
|
+
# came straight from the DB — data, not a hardcoded UI string.
|
|
150
|
+
@data_sourced = (@data_sourced || 0) + 1
|
|
151
|
+
else
|
|
152
|
+
@unwrapped = (@unwrapped || 0) + 1
|
|
153
|
+
(hits[url] ||= []) << "[#{key}] #{value.inspect}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Verbatim string values from the fixtures. A candidate equal to one of
|
|
159
|
+
# these is an ActiveRecord display value (loaded from the DB, so it
|
|
160
|
+
# never went through t() by design) rather than hardcoded copy, and is
|
|
161
|
+
# excluded to cut the bulk of the AR noise. Exact match only — a
|
|
162
|
+
# hardcoded string that happens to equal a fixture value would be
|
|
163
|
+
# skipped too, an acceptable trade for ~85% less noise. Built once;
|
|
164
|
+
# fixtures contain no ERB so static YAML parsing is exact.
|
|
165
|
+
def fixture_strings
|
|
166
|
+
@fixture_strings ||= Dir[File.join(fixture_dir, '**', '*.yml')].each_with_object(Set.new) do |file, set|
|
|
167
|
+
data = YAML.safe_load(File.read(file), permitted_classes: [Date, Time], aliases: true)
|
|
168
|
+
collect_strings(data, set)
|
|
169
|
+
rescue Psych::Exception
|
|
170
|
+
next
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def collect_strings(node, set)
|
|
175
|
+
case node
|
|
176
|
+
when Hash then node.each_value { |v| collect_strings(v, set) }
|
|
177
|
+
when Array then node.each { |v| collect_strings(v, set) }
|
|
178
|
+
when String
|
|
179
|
+
s = node.strip
|
|
180
|
+
set << s unless s.empty?
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def diag
|
|
185
|
+
"wrapped(localized via t)=#{@wrapped || 0} data-sourced(fixtures, skipped)=#{@data_sourced || 0} " \
|
|
186
|
+
"unwrapped(candidates)=#{@unwrapped || 0}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def walk(node, &blk)
|
|
190
|
+
case node
|
|
191
|
+
when Hash
|
|
192
|
+
node.each do |k, v|
|
|
193
|
+
blk.call(k, v) if TEXT_KEYS.include?(k) && v.is_a?(String)
|
|
194
|
+
walk(v, &blk)
|
|
195
|
+
end
|
|
196
|
+
when Array
|
|
197
|
+
node.each { |v| walk(v, &blk) }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Skip values that are not copy: blank, no letters (numbers/units/
|
|
202
|
+
# symbols), or ALL_CAPS code identifiers. Tune as real noise surfaces.
|
|
203
|
+
def trivial?(str)
|
|
204
|
+
s = str.strip
|
|
205
|
+
s.empty? || !s.match?(/[A-Za-z]/) || s.match?(/\A[A-Z0-9_]+\z/)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def crawl_pseudo(user)
|
|
212
|
+
user[:token] = login(user)
|
|
213
|
+
|
|
214
|
+
router = Glib::JsonCrawler::Router.new
|
|
215
|
+
http = Glib::JsonCrawler::Http.new(self, user, router, inspect_result: true)
|
|
216
|
+
router.host = HOST
|
|
217
|
+
router.skip_similar_page = true
|
|
218
|
+
|
|
219
|
+
ENV['CRAWLER_MODE'] = 'true'
|
|
220
|
+
router.step(
|
|
221
|
+
http,
|
|
222
|
+
'onClick' => {
|
|
223
|
+
'action' => 'initiate_navigation',
|
|
224
|
+
'url' => "http://#{HOST}/users/me?format=json&redirect=default"
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
ensure
|
|
228
|
+
ENV['CRAWLER_MODE'] = nil
|
|
229
|
+
logout
|
|
230
|
+
report!(user)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def report!(user)
|
|
234
|
+
hits = self.class.hits
|
|
235
|
+
report_dir = self.class.report_dir
|
|
236
|
+
FileUtils.mkdir_p(report_dir)
|
|
237
|
+
path = File.join(report_dir, "#{user[:email]}.txt")
|
|
238
|
+
|
|
239
|
+
lines = []
|
|
240
|
+
hits.sort.each do |url, values|
|
|
241
|
+
lines << url
|
|
242
|
+
values.uniq.each { |v| lines << " #{v}" }
|
|
243
|
+
lines << ''
|
|
244
|
+
end
|
|
245
|
+
total = hits.values.sum { |v| v.uniq.size }
|
|
246
|
+
summary = "#{total} hardcoded candidate(s) across #{hits.size} page(s) for #{user[:email]}"
|
|
247
|
+
|
|
248
|
+
File.write(path, ([summary, ''] + lines).join("\n"))
|
|
249
|
+
puts "\n[pseudo_i18n] #{summary}\n report: #{path}"
|
|
250
|
+
puts "[pseudo_i18n] DIAG: #{self.class.diag}"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -2,51 +2,44 @@ module RuboCop
|
|
|
2
2
|
module Cop
|
|
3
3
|
module DevDoc
|
|
4
4
|
module Rails
|
|
5
|
-
# Rails `enum` must
|
|
6
|
-
#
|
|
5
|
+
# Every Rails `enum` declaration must be paired with `enum_symbolize`
|
|
6
|
+
# so the reader returns a symbol instead of Rails' default string form.
|
|
7
7
|
#
|
|
8
8
|
# ## Rationale
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# DB — Rails raises "Undeclared attribute type for enum". Declaring
|
|
15
|
-
# the attribute up front (which `enum_symbolize` does, from the enum's
|
|
16
|
-
# own values) removes that dependency on the column.
|
|
17
|
-
# 2. **Reader.** `enum` defines a reader that returns the string form, so
|
|
18
|
-
# `record.status == :active` silently fails (`:foo == 'foo'` is
|
|
19
|
-
# false). `enum_symbolize` overrides the reader to return a symbol.
|
|
20
|
-
#
|
|
21
|
-
# The `enum_symbolize :name, { … }` form does both in one call — it
|
|
22
|
-
# declares the attribute, defines the enum, adds a presence validation,
|
|
23
|
-
# and symbolizes the reader — so neither footgun can be reintroduced by
|
|
24
|
-
# forgetting a pairing line.
|
|
9
|
+
# Strings and symbols are not equal in Ruby (`:foo == 'foo'` is `false`).
|
|
10
|
+
# Rails' `enum` macro defines a reader that returns the string form, so
|
|
11
|
+
# downstream comparisons like `record.status == :active` silently fail.
|
|
12
|
+
# Pairing `enum :status, …` with `enum_symbolize :status` overrides the
|
|
13
|
+
# reader to hand back a symbol, eliminating the footgun.
|
|
25
14
|
#
|
|
26
15
|
# See `best_practices/backend/en/01a_defensive_programming.md` item 7.
|
|
27
16
|
#
|
|
28
17
|
# ❌
|
|
29
18
|
# class Order < ApplicationRecord
|
|
30
19
|
# enum :payment_status, { draft: 0, pending: 1, finalized: 2 }
|
|
31
|
-
# enum_symbolize :payment_status
|
|
32
20
|
# end
|
|
33
21
|
#
|
|
34
22
|
# ✔
|
|
35
23
|
# class Order < ApplicationRecord
|
|
36
|
-
#
|
|
24
|
+
# enum :payment_status, { draft: 0, pending: 1, finalized: 2 }
|
|
25
|
+
#
|
|
26
|
+
# enum_symbolize :payment_status
|
|
37
27
|
# end
|
|
38
28
|
#
|
|
39
|
-
#
|
|
29
|
+
# `enum_symbolize` accepts multiple names so several enums can be
|
|
30
|
+
# paired in one call:
|
|
40
31
|
#
|
|
41
32
|
# ✔
|
|
42
|
-
# enum_symbolize :
|
|
33
|
+
# enum_symbolize :payment_status, :finalize_intent
|
|
43
34
|
#
|
|
44
35
|
# Enums declared inside a concern's `included do` block are checked the
|
|
45
|
-
# same way
|
|
36
|
+
# same way — the pairing must live in the same `included do` block,
|
|
37
|
+
# which is also where it belongs so every includer gets both:
|
|
46
38
|
#
|
|
47
39
|
# ✔
|
|
48
40
|
# included do
|
|
49
|
-
#
|
|
41
|
+
# enum :discount_type, DISCOUNT_TYPES
|
|
42
|
+
# enum_symbolize :discount_type
|
|
50
43
|
# end
|
|
51
44
|
#
|
|
52
45
|
# @example
|
|
@@ -54,16 +47,20 @@ module RuboCop
|
|
|
54
47
|
# enum :status, { active: 0, archived: 1 }
|
|
55
48
|
#
|
|
56
49
|
# # good
|
|
57
|
-
#
|
|
50
|
+
# enum :status, { active: 0, archived: 1 }
|
|
51
|
+
# enum_symbolize :status
|
|
58
52
|
class EnumMustBeSymbolized < Base
|
|
59
|
-
MSG = '
|
|
60
|
-
'
|
|
61
|
-
'reader returns a symbol (see backend/01a_defensive_programming.md item 7).'.freeze
|
|
53
|
+
MSG = 'Pair `enum :%<name>s` with `enum_symbolize :%<name>s` so the reader returns a symbol ' \
|
|
54
|
+
'(see backend/01a_defensive_programming.md item 7).'.freeze
|
|
62
55
|
|
|
63
56
|
def_node_matcher :enum_call, <<~PATTERN
|
|
64
57
|
(send nil? :enum (sym $_) ...)
|
|
65
58
|
PATTERN
|
|
66
59
|
|
|
60
|
+
def_node_matcher :enum_symbolize_call, <<~PATTERN
|
|
61
|
+
(send nil? :enum_symbolize $...)
|
|
62
|
+
PATTERN
|
|
63
|
+
|
|
67
64
|
def_node_matcher :included_block?, <<~PATTERN
|
|
68
65
|
(block (send nil? :included) (args) _)
|
|
69
66
|
PATTERN
|
|
@@ -86,15 +83,28 @@ module RuboCop
|
|
|
86
83
|
return unless body
|
|
87
84
|
|
|
88
85
|
statements = body.begin_type? ? body.children : [body]
|
|
89
|
-
|
|
90
|
-
next unless stmt.respond_to?(:send_type?) && stmt.send_type?
|
|
86
|
+
enum_decls, symbolized = scan(statements)
|
|
91
87
|
|
|
92
|
-
|
|
93
|
-
next
|
|
88
|
+
enum_decls.each do |name, decl|
|
|
89
|
+
next if symbolized.include?(name)
|
|
94
90
|
|
|
95
|
-
add_offense(
|
|
91
|
+
add_offense(decl, message: format(MSG, name: name))
|
|
96
92
|
end
|
|
97
93
|
end
|
|
94
|
+
|
|
95
|
+
def scan(statements)
|
|
96
|
+
sends = statements.select { |stmt| stmt.respond_to?(:send_type?) && stmt.send_type? }
|
|
97
|
+
enum_decls = sends.filter_map { |stmt| (name = enum_call(stmt)) && [name, stmt] }
|
|
98
|
+
symbolized = sends.flat_map { |stmt| symbolized_names(stmt) }
|
|
99
|
+
[enum_decls, symbolized]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def symbolized_names(stmt)
|
|
103
|
+
args = enum_symbolize_call(stmt)
|
|
104
|
+
return [] unless args
|
|
105
|
+
|
|
106
|
+
args.select(&:sym_type?).map(&:value)
|
|
107
|
+
end
|
|
98
108
|
end
|
|
99
109
|
end
|
|
100
110
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-dev_doc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dev-doc contributors
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
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: activesupport
|
|
@@ -80,19 +79,19 @@ dependencies:
|
|
|
80
79
|
- - ">="
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
81
|
version: '2.29'
|
|
83
|
-
description:
|
|
84
|
-
email:
|
|
85
82
|
executables: []
|
|
86
83
|
extensions: []
|
|
87
84
|
extra_rdoc_files: []
|
|
88
85
|
files:
|
|
89
86
|
- config/default.yml
|
|
90
87
|
- config/obsoletion.yml
|
|
88
|
+
- lib/dev_doc/i18n/pseudo_locale.rb
|
|
91
89
|
- lib/dev_doc/test/best_practice_lints.rb
|
|
92
90
|
- lib/dev_doc/test/lints/cron_schedule.rb
|
|
93
91
|
- lib/dev_doc/test/lints/duplicate_snapshot.rb
|
|
94
92
|
- lib/dev_doc/test/lints/http_driven_controller_tests.rb
|
|
95
93
|
- lib/dev_doc/test/lints/no_file_excludes.rb
|
|
94
|
+
- lib/dev_doc/test/pseudo_i18n_crawler.rb
|
|
96
95
|
- lib/rubocop-dev_doc.rb
|
|
97
96
|
- lib/rubocop/cop/dev_doc/auth/current_user_branching.rb
|
|
98
97
|
- lib/rubocop/cop/dev_doc/auth/load_resource_current_user_guard.rb
|
|
@@ -158,12 +157,10 @@ files:
|
|
|
158
157
|
- lib/rubocop/dev_doc.rb
|
|
159
158
|
- lib/rubocop/dev_doc/plugin.rb
|
|
160
159
|
- lib/rubocop/dev_doc/version.rb
|
|
161
|
-
homepage:
|
|
162
160
|
licenses: []
|
|
163
161
|
metadata:
|
|
164
162
|
default_lint_roller_plugin: RuboCop::DevDoc::Plugin
|
|
165
163
|
rubygems_mfa_required: 'true'
|
|
166
|
-
post_install_message:
|
|
167
164
|
rdoc_options: []
|
|
168
165
|
require_paths:
|
|
169
166
|
- lib
|
|
@@ -178,8 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
178
175
|
- !ruby/object:Gem::Version
|
|
179
176
|
version: '0'
|
|
180
177
|
requirements: []
|
|
181
|
-
rubygems_version:
|
|
182
|
-
signing_key:
|
|
178
|
+
rubygems_version: 4.0.6
|
|
183
179
|
specification_version: 4
|
|
184
180
|
summary: RuboCop cops enforcing dev-doc best practices
|
|
185
181
|
test_files: []
|