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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 245c7101b5dad69bf73859eea7a5d7c9027f1573d4e1b4ccbcde2bed3e61266f
4
- data.tar.gz: 585c169414705979b1c45bf02ad87de324c5e6a16fb6a8ddb0e631961820e533
3
+ metadata.gz: 5635cb76266896b0fb2d2e661844d27400be25b6e4a1190271279c7a4331517c
4
+ data.tar.gz: d8afd8c82d444afd15e951eba3aae5d1daaa0fce73efd7ac86fe725b9ce6dd09
5
5
  SHA512:
6
- metadata.gz: c5dfa80d58dbf2cc918f7a0ea2b74937156356137abca9c7297f0a974c1ee079176f335b6a5d33599f81f9d6af8f8efe324d9ab841f52126750bc80fb01abb38
7
- data.tar.gz: 4a6b23ac1e990068f529e2ad7ed54fa078391256894d59b8a2390b5a9e773fd26a0dc55b11dcd3bfb00e13548791e80bcf1a5659bdd2ca82d26a5db1495893f1
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 library, so a name declared in one
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
- # Mappings replaces the built-in defaults when set:
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
- # Libraries merges over the built-in defaults:
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Glyphs
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  end
@@ -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, _component, library)
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"] || "app/assets/svg/icons", Dir.pwd)
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
- @available_icons_cache[[base_path, library_dir, variant]] ||= load_icons(base_path, library_dir, variant)
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(base_path, library_dir, variant)
277
- path = File.join(*[base_path, library_dir, variant].compact.reject { |part| part.to_s.empty? })
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glyphs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson