glyphs 0.2.2 → 0.2.4
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 +37 -1
- data/README.md +31 -3
- data/config/default.yml +13 -4
- data/lib/glyphs/source_scanner.rb +35 -0
- data/lib/glyphs/version.rb +1 -1
- data/lib/rubocop/cop/glyphs/icon_resolution.rb +54 -6
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5635cb76266896b0fb2d2e661844d27400be25b6e4a1190271279c7a4331517c
|
|
4
|
+
data.tar.gz: d8afd8c82d444afd15e951eba3aae5d1daaa0fce73efd7ac86fe725b9ce6dd09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dffab87c9e9862dad15042135dc4c2ef7fb5785dd848210538b6e63c5263b8be6fc4a9b6c5276ac8ee7523fe239e08bb63007aff45af8ba52721fd52d774a946
|
|
7
|
+
data.tar.gz: 492c537c6ebb87013ec9795406b78cfb6b50e06328e2cc9b8b072a0c359e71d575a6d4853450c66b7d3c526492679ad156ef858a122cf848f4856ae957b14e55
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`Glyphs/IconResolution` no longer fails open on a missing icon directory.**
|
|
8
|
+
A directory that did not exist produced the same empty list as one that
|
|
9
|
+
existed with no SVGs, and both were read as "nothing to check" — so a library
|
|
10
|
+
that was never synced, or a `DefaultVariant` naming a variant the project does
|
|
11
|
+
not have, silently disabled the cop for every call site of that library. Green
|
|
12
|
+
cop, green CI, no validation at all, indefinitely. `load_icons` now returns
|
|
13
|
+
`nil` for an absent directory and `[]` for an empty one; the absent case warns
|
|
14
|
+
once per library/variant, naming the path. The synced-but-empty case still
|
|
15
|
+
passes silently. Refs #8
|
|
16
|
+
|
|
17
|
+
- **Cop options are declared, so RuboCop stops calling them unsupported.**
|
|
18
|
+
`config/default.yml` documented `Libraries` (`Glyphs/IconResolution`) and
|
|
19
|
+
`Mappings` (`Glyphs/LegacyIconHelper`) only in comments, and never mentioned
|
|
20
|
+
`LibraryComponents` (`Glyphs/LegacyIconHelper`, `Glyphs/PreferLibraryComponent`)
|
|
21
|
+
at all. RuboCop derives its supported-parameter list from the keys actually
|
|
22
|
+
present in that file, so it warned `does not support <param> parameter` on
|
|
23
|
+
every run — for four working options, one of which
|
|
24
|
+
`Glyphs/LegacyIconHelper` itself tells you to add. Each is now declared with an
|
|
25
|
+
empty default (a no-op merge), so the options are validated instead of
|
|
26
|
+
reported, with behaviour unchanged. Refs #7
|
|
27
|
+
|
|
28
|
+
- **Declaration harvest unwraps trailing `.freeze`.** `ICONS = { "x" => :car }.freeze`
|
|
29
|
+
(and `%i[a b].freeze`) used to yield a Prism `CallNode`, so hash/array values
|
|
30
|
+
were never collected. Cross-file dynamics (`PhosphorIcon(@icon)` in a shared
|
|
31
|
+
component + names only in a frozen `ICONS` map) then lost those icons at
|
|
32
|
+
`glyphs:prune_icons`. `collect_declaration` now unwraps zero-arg `.freeze`
|
|
33
|
+
(and parentheses) before walking the value.
|
|
34
|
+
|
|
5
35
|
### Added
|
|
6
36
|
|
|
37
|
+
- **`Glyphs/IconResolution` gained a `Strict` option** (default `false`). It
|
|
38
|
+
escalates the missing-icon-directory warning above to an offence, so projects
|
|
39
|
+
that depend on this cop can fail closed in CI instead of trusting a warning
|
|
40
|
+
not to scroll past.
|
|
41
|
+
|
|
7
42
|
- **Dynamic icon calls are now resolved from source.** `SourceScanner` no longer
|
|
8
43
|
silently skips `LucideIcon(some_var)` / `PhosphorIcon(tile[:icon])` — it harvests
|
|
9
44
|
the literal name from two places so the pruner keeps it:
|
|
@@ -13,7 +48,8 @@
|
|
|
13
48
|
- _declaration-based_: literals in icon-declaration positions anywhere (a hash
|
|
14
49
|
pair keyed `/icon/i`, or a constant named `/ICON/`) are kept for every
|
|
15
50
|
dynamically-rendered library, closing the cross-file gap (e.g. a notifier
|
|
16
|
-
`ICON = :bell` constant rendered from a view).
|
|
51
|
+
`ICON = :bell` constant rendered from a view). Frozen maps
|
|
52
|
+
(`ICONS = { … }.freeze`) are included — see Fixed above.
|
|
17
53
|
|
|
18
54
|
This makes `keep_icons` a last-resort escape hatch (DB/ENV/gem-chrome names)
|
|
19
55
|
rather than the primary mechanism. Only literals are harvested, so the scanner
|
data/README.md
CHANGED
|
@@ -133,9 +133,9 @@ The scanner resolves them from source, so you rarely need `keep_icons` at all:
|
|
|
133
133
|
ternaries (`@open ? "caret-up" : "caret-down"`), `case/when`, and locals.
|
|
134
134
|
- **Declaration-based** — literals in icon-declaration positions *anywhere* — a
|
|
135
135
|
hash pair keyed like an icon (`icon: :gear`, `menu_icon: "house"`) or a
|
|
136
|
-
constant named like one (`ICON = :bell`, `STATUS_ICONS = { .. => :warning }
|
|
137
|
-
are kept for every dynamically-rendered
|
|
138
|
-
file and rendered from another survives.
|
|
136
|
+
constant named like one (`ICON = :bell`, `STATUS_ICONS = { .. => :warning }`,
|
|
137
|
+
including trailing `.freeze`) — are kept for every dynamically-rendered
|
|
138
|
+
library, so a name declared in one file and rendered from another survives.
|
|
139
139
|
|
|
140
140
|
Only literals are harvested, so the scanner never invents a reference; the
|
|
141
141
|
worst case is keeping a coincidentally icon-named string, which the post-prune
|
|
@@ -210,6 +210,10 @@ Glyphs/LegacyIconHelper:
|
|
|
210
210
|
# Mappings replaces the built-in defaults (_lucide/_phosphor/_hero/_heroicon/_tabler):
|
|
211
211
|
# Mappings:
|
|
212
212
|
# _custom: CustomIcon
|
|
213
|
+
# LibraryComponents merges over the built-in library => component map, so
|
|
214
|
+
# `icon("x", library: "customlib")` corrects to `CustomIcon("x")`:
|
|
215
|
+
# LibraryComponents:
|
|
216
|
+
# customlib: CustomIcon
|
|
213
217
|
```
|
|
214
218
|
|
|
215
219
|
### Glyphs/IconResolution
|
|
@@ -231,14 +235,38 @@ Glyphs/IconResolution:
|
|
|
231
235
|
# Libraries merges over the built-in defaults (component => Dir/DefaultVariant):
|
|
232
236
|
Libraries:
|
|
233
237
|
PhosphorIcon: { Dir: phosphor, DefaultVariant: regular }
|
|
238
|
+
# Escalate a missing icon directory from a warning to an offence:
|
|
239
|
+
Strict: false
|
|
234
240
|
```
|
|
235
241
|
|
|
242
|
+
If the directory a library resolves to does not exist — the library was never synced, or
|
|
243
|
+
`DefaultVariant` names a variant you do not have — the cop cannot validate any of that
|
|
244
|
+
library's call sites. Rather than pass silently, it warns once per library/variant:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
[Glyphs/IconResolution] Icon directory `app/assets/svg/icons/phosphor/regular` not found,
|
|
248
|
+
so `PhosphorIcon` names are not validated. Sync the library or fix `IconsPath`/`Libraries`.
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Set `Strict: true` to make that an offence instead, so CI fails closed rather than reporting
|
|
252
|
+
green while checking nothing. A directory that exists but ships no SVGs is not a
|
|
253
|
+
misconfiguration and stays silent either way.
|
|
254
|
+
|
|
236
255
|
### Glyphs/PreferLibraryComponent
|
|
237
256
|
|
|
238
257
|
```ruby
|
|
239
258
|
Icon(:house, library: :lucide) # => LucideIcon(:house)
|
|
240
259
|
```
|
|
241
260
|
|
|
261
|
+
```yaml
|
|
262
|
+
Glyphs/PreferLibraryComponent:
|
|
263
|
+
Include:
|
|
264
|
+
- app/**/*.rb
|
|
265
|
+
# LibraryComponents merges over the built-in library => component map:
|
|
266
|
+
# LibraryComponents:
|
|
267
|
+
# customlib: CustomIcon
|
|
268
|
+
```
|
|
269
|
+
|
|
242
270
|
## Migrating an app off icon helpers
|
|
243
271
|
|
|
244
272
|
1. Add `gem "glyphs"`, `include Glyphs` where the helpers used to be included.
|
data/config/default.yml
CHANGED
|
@@ -3,10 +3,13 @@ Glyphs/LegacyIconHelper:
|
|
|
3
3
|
Enabled: true
|
|
4
4
|
VersionAdded: '0.1.0'
|
|
5
5
|
SafeAutoCorrect: true
|
|
6
|
-
#
|
|
7
|
-
# Mappings:
|
|
6
|
+
# Replaces the built-in helper => component defaults when set, e.g.
|
|
8
7
|
# _lucide: LucideIcon
|
|
9
8
|
# _heroicon: HeroIcon
|
|
9
|
+
Mappings: {}
|
|
10
|
+
# Merges over the built-in library => component map, e.g.
|
|
11
|
+
# customlib: CustomIcon
|
|
12
|
+
LibraryComponents: {}
|
|
10
13
|
DefaultLibraryComponent: HeroIcon
|
|
11
14
|
|
|
12
15
|
Glyphs/IconResolution:
|
|
@@ -14,12 +17,18 @@ Glyphs/IconResolution:
|
|
|
14
17
|
Enabled: true
|
|
15
18
|
VersionAdded: '0.1.0'
|
|
16
19
|
IconsPath: app/assets/svg/icons
|
|
17
|
-
#
|
|
18
|
-
# Libraries:
|
|
20
|
+
# Merges over the built-in component => directory/variant defaults, e.g.
|
|
19
21
|
# PhosphorIcon: { Dir: phosphor, DefaultVariant: light }
|
|
22
|
+
Libraries: {}
|
|
23
|
+
# An icon directory that does not exist warns once per library/variant.
|
|
24
|
+
# Strict turns that warning into an offence so CI fails closed.
|
|
25
|
+
Strict: false
|
|
20
26
|
|
|
21
27
|
Glyphs/PreferLibraryComponent:
|
|
22
28
|
Description: 'Prefer library-specific components over generic Icon(..., library: ...).'
|
|
23
29
|
Enabled: true
|
|
24
30
|
VersionAdded: '0.1.0'
|
|
25
31
|
SafeAutoCorrect: true
|
|
32
|
+
# Merges over the built-in library => component map, e.g.
|
|
33
|
+
# customlib: CustomIcon
|
|
34
|
+
LibraryComponents: {}
|
|
@@ -346,7 +346,11 @@ module Glyphs
|
|
|
346
346
|
|
|
347
347
|
# Harvests icon names from a declaration value: a bare literal, an array of
|
|
348
348
|
# literals (`ICON = %i[a b]`), or a hash's values (`{ "sms" => :device }`).
|
|
349
|
+
# Trailing `.freeze` is unwrapped so `ICONS = { … }.freeze` still harvests —
|
|
350
|
+
# without this, cross-file dynamics (`PhosphorIcon(@icon)` + names only in a
|
|
351
|
+
# frozen `ICONS` hash) lose those names at prune.
|
|
349
352
|
def collect_declaration(value_node)
|
|
353
|
+
value_node = unwrap_declaration_wrappers(value_node)
|
|
350
354
|
case value_node
|
|
351
355
|
when Prism::SymbolNode, Prism::StringNode
|
|
352
356
|
collect_literal(@declaration_literals, value_node)
|
|
@@ -356,6 +360,37 @@ module Glyphs
|
|
|
356
360
|
value_node.elements.grep(Prism::AssocNode).each { |assoc| collect_declaration(assoc.value) }
|
|
357
361
|
end
|
|
358
362
|
end
|
|
363
|
+
|
|
364
|
+
# Peel zero-arg `.freeze` and parentheses so the underlying Hash/Array/literal
|
|
365
|
+
# is what declaration harvest walks.
|
|
366
|
+
def unwrap_declaration_wrappers(value_node)
|
|
367
|
+
loop do
|
|
368
|
+
case value_node
|
|
369
|
+
when Prism::CallNode
|
|
370
|
+
break unless freeze_call?(value_node)
|
|
371
|
+
|
|
372
|
+
value_node = value_node.receiver
|
|
373
|
+
when Prism::ParenthesesNode
|
|
374
|
+
value_node = value_node.body
|
|
375
|
+
when Prism::StatementsNode
|
|
376
|
+
# `(expr)` → StatementsNode with one child; multi-statement bodies are
|
|
377
|
+
# not valid constant values, so take the sole statement when present.
|
|
378
|
+
break unless value_node.body.size == 1
|
|
379
|
+
|
|
380
|
+
value_node = value_node.body.first
|
|
381
|
+
else
|
|
382
|
+
break
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
value_node
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# `receiver.freeze` with no args — the common immutable-constant pattern.
|
|
389
|
+
def freeze_call?(node)
|
|
390
|
+
node.name == :freeze &&
|
|
391
|
+
node.receiver &&
|
|
392
|
+
(node.arguments.nil? || node.arguments.arguments.empty?)
|
|
393
|
+
end
|
|
359
394
|
end
|
|
360
395
|
end
|
|
361
396
|
end
|
data/lib/glyphs/version.rb
CHANGED
|
@@ -11,6 +11,11 @@ module RuboCop
|
|
|
11
11
|
# calls (`_lucide(:house)`) are validated. A literal `variant:` keyword is
|
|
12
12
|
# honored; calls with dynamic names or variants are skipped.
|
|
13
13
|
#
|
|
14
|
+
# A directory that does not exist is a configuration error, not "nothing
|
|
15
|
+
# to check": it warns once per library/variant, or — with `Strict` — adds
|
|
16
|
+
# an offence, so the cop can never quietly validate nothing. A directory
|
|
17
|
+
# that exists but ships no SVGs stays silent.
|
|
18
|
+
#
|
|
14
19
|
# @example
|
|
15
20
|
# # bad
|
|
16
21
|
# LucideIcon(:non_existent_icon)
|
|
@@ -51,10 +56,14 @@ module RuboCop
|
|
|
51
56
|
|
|
52
57
|
ICONIFY_PATTERN = /\biconify\s+(lucide|phosphor|heroicons)--([a-z0-9-]+)/
|
|
53
58
|
|
|
59
|
+
DEFAULT_ICONS_PATH = "app/assets/svg/icons"
|
|
60
|
+
|
|
54
61
|
RAW_MSG = "Use `%{component}(:%{symbol}, class: \"%{rest}\")` instead of raw `iconify %{library}--…` class."
|
|
55
62
|
RAW_DSTR_MSG = "Use `LucideIcon(name, class: ...)` etc. instead of building a raw " \
|
|
56
63
|
"`iconify <library>--…` class string."
|
|
57
64
|
MISSING_MSG = "Icon `%{name}` not found in %{library}/%{variant}. %{suggestion}"
|
|
65
|
+
MISSING_DIR_MSG = "Icon directory `%{path}` not found, so `%{component}` names are not validated. " \
|
|
66
|
+
"Sync the library or fix `IconsPath`/`Libraries`."
|
|
58
67
|
|
|
59
68
|
def on_str(node)
|
|
60
69
|
return if node.parent&.dstr_type?
|
|
@@ -97,7 +106,7 @@ module RuboCop
|
|
|
97
106
|
@libraries ||= DEFAULT_LIBRARIES.merge(cop_config["Libraries"] || {})
|
|
98
107
|
end
|
|
99
108
|
|
|
100
|
-
def check_call(node,
|
|
109
|
+
def check_call(node, component, library)
|
|
101
110
|
name = literal_name(node.first_argument)
|
|
102
111
|
return unless name
|
|
103
112
|
|
|
@@ -105,6 +114,7 @@ module RuboCop
|
|
|
105
114
|
return if variant == :dynamic
|
|
106
115
|
|
|
107
116
|
available = available_icons(library["Dir"], variant)
|
|
117
|
+
return report_missing_directory(node, component, library["Dir"], variant) if available.nil?
|
|
108
118
|
return if available.empty?
|
|
109
119
|
return if available.include?(name)
|
|
110
120
|
|
|
@@ -124,6 +134,21 @@ module RuboCop
|
|
|
124
134
|
end
|
|
125
135
|
end
|
|
126
136
|
|
|
137
|
+
# The directory the cop was told to read is missing, so every call for
|
|
138
|
+
# this library goes unchecked. Report it rather than pass silently.
|
|
139
|
+
def report_missing_directory(node, component, library_dir, variant)
|
|
140
|
+
message = format(MISSING_DIR_MSG, path: configured_directory(library_dir, variant), component:)
|
|
141
|
+
return add_offense(node.first_argument, message:) if cop_config["Strict"]
|
|
142
|
+
|
|
143
|
+
self.class.warn_once("[Glyphs/IconResolution] #{message}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Built from the unexpanded `IconsPath` so the message shows the path as
|
|
147
|
+
# the project wrote it, not an absolute machine-specific one.
|
|
148
|
+
def configured_directory(library_dir, variant)
|
|
149
|
+
self.class.directory_for(cop_config["IconsPath"] || DEFAULT_ICONS_PATH, library_dir, variant)
|
|
150
|
+
end
|
|
151
|
+
|
|
127
152
|
def variant_for(node, library)
|
|
128
153
|
pair = variant_pair(node)
|
|
129
154
|
return library["DefaultVariant"] if pair.nil?
|
|
@@ -258,7 +283,7 @@ module RuboCop
|
|
|
258
283
|
end
|
|
259
284
|
|
|
260
285
|
def icons_base_path
|
|
261
|
-
@icons_base_path ||= File.expand_path(cop_config["IconsPath"] ||
|
|
286
|
+
@icons_base_path ||= File.expand_path(cop_config["IconsPath"] || DEFAULT_ICONS_PATH, Dir.pwd)
|
|
262
287
|
end
|
|
263
288
|
|
|
264
289
|
def available_icons(library_dir, variant)
|
|
@@ -266,16 +291,39 @@ module RuboCop
|
|
|
266
291
|
end
|
|
267
292
|
|
|
268
293
|
class << self
|
|
294
|
+
# nil when the directory is absent, [] when it exists but ships no
|
|
295
|
+
# SVGs — the caller has to tell those apart. Memoized with `key?` (not
|
|
296
|
+
# `||=`) so a nil result is not re-probed on every call site.
|
|
269
297
|
def available_icons_for(base_path, library_dir, variant)
|
|
270
298
|
@available_icons_cache ||= {}
|
|
271
|
-
|
|
299
|
+
key = [base_path, library_dir, variant]
|
|
300
|
+
return @available_icons_cache[key] if @available_icons_cache.key?(key)
|
|
301
|
+
|
|
302
|
+
@available_icons_cache[key] = load_icons(directory_for(base_path, library_dir, variant))
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def directory_for(base_path, library_dir, variant)
|
|
306
|
+
File.join(*[base_path, library_dir, variant].compact.reject { |part| part.to_s.empty? })
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# RuboCop builds a fresh cop instance per file, so deduplicating the
|
|
310
|
+
# missing-directory warning has to outlive the instance.
|
|
311
|
+
def warn_once(message)
|
|
312
|
+
@warned_messages ||= {}
|
|
313
|
+
return if @warned_messages.key?(message)
|
|
314
|
+
|
|
315
|
+
@warned_messages[message] = true
|
|
316
|
+
warn message
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def reset_warnings!
|
|
320
|
+
@warned_messages = nil
|
|
272
321
|
end
|
|
273
322
|
|
|
274
323
|
private
|
|
275
324
|
|
|
276
|
-
def load_icons(
|
|
277
|
-
|
|
278
|
-
return [] unless Dir.exist?(path)
|
|
325
|
+
def load_icons(path)
|
|
326
|
+
return nil unless Dir.exist?(path)
|
|
279
327
|
|
|
280
328
|
Dir.children(path).filter_map { |file| File.basename(file, ".svg") if file.end_with?(".svg") }.sort
|
|
281
329
|
end
|