Module: Lilac::Directives::Lints
- Defined in:
- lib/lilac/directives/lints.rb,
lib/lilac/directives/collision_rules.rb
Overview
Collision rules SSOT — duplicate pair (build-time / runtime). See decisions §17.
The data here is consumed by both halves of Lints:
- build-time: `Lilac::Directives::Lints.check!` (raises)
- runtime: `Lilac::Directives::Lints.check!` (warn+skip /
raise depending on severity)
Each row in COLLISION_PAIRS is [attrs, message] where attrs
is the unordered pair of data-* attribute names that may not
coexist on the same element, and message is the human-readable
rationale used in both build-error and runtime warning text.
Attribute-name form (not Symbol kinds) lets the same rules apply
uniformly to built-in directives and class-based Handler packages
(ADR-0027) — the public surface a user actually writes is the
attribute name, so matching against it removes the Symbol-pivot
indirection on both sides.
Defined Under Namespace
Classes: Error
Constant Summary collapse
- COLLISION_PAIRS =
[ [ %w[data-text data-unsafe-html], "data-text and data-unsafe-html cannot coexist (both write the element body)", ], [ %w[data-text data-each], "data-text and data-each cannot coexist (data-each generates children; data-text would overwrite them)", ], [ %w[data-unsafe-html data-each], "data-unsafe-html and data-each cannot coexist (data-each generates children; data-unsafe-html would overwrite them)", ], [ %w[data-show data-hide], "data-show and data-hide cannot coexist (use one; the inverse is implicit)", ], [ %w[data-component data-each], "data-component and data-each cannot coexist (wrap with another element — put the child component inside the iteration body)", ], [ %w[data-bind data-field], "data-bind and data-field cannot coexist (both wire the input value — pick one: data-bind for form-independent binding, data-field for form-scope binding)", ], ].freeze
Class Method Summary collapse
-
.attribute_for(directive) ⇒ Object
Derive the
data-*attribute name from a TemplateAST directive. - .check!(directives, file:) ⇒ Object
- .check_collisions(dirs, file) ⇒ Object
-
.check_form_scope!(directives, file) ⇒ Object
Scope rules for the form gem (form-spec §8 / §10.2): -
data-formis only allowed on<form>elements - multiple bare<form>(no data-form attr) within the same component would collide on the:defaultscope, so flag the second occurrence as an error. -
.check_gn_hidden_conflict(dirs, file) ⇒ Object
lil-hiddenis reserved by data-show / data-hide.
Class Method Details
.attribute_for(directive) ⇒ Object
Derive the data-* attribute name from a TemplateAST directive.
Mirrors the runtime helper of the same name: both built-in
directive kinds and CLI-registered emitter kinds (form's
:form / :field / :button) follow the data-<kind> rule
with _ → - and a trailing _ stripped.
90 91 92 |
# File 'lib/lilac/directives/lints.rb', line 90 def self.attribute_for(directive) "data-#{directive.kind.to_s.tr('_', '-').chomp('-')}" end |
.check!(directives, file:) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/lilac/directives/lints.rb', line 22 def self.check!(directives, file:) # Group by (scope_id, ref_id) so directives that share a `lilN` # NAME but live in different ref scopes (top-level vs each # iteration body) are not falsely flagged as colliding. With # the per-scope positional counter (decisions §19), ref_ids are # only unique WITHIN a scope. directives.group_by { |d| [d.scope_id, d.ref_id] }.each_value do |dirs_on_element| check_collisions(dirs_on_element, file) check_gn_hidden_conflict(dirs_on_element, file) end check_form_scope!(directives, file) end |
.check_collisions(dirs, file) ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/lilac/directives/lints.rb', line 70 def self.check_collisions(dirs, file) attr_names = dirs.map { |d| attribute_for(d) } COLLISION_PAIRS.each do |pair, | next unless pair.all? { |a| attr_names.include?(a) } # Report at the line of the second (later) directive in the # pair so users see where the conflict was introduced. offenders = dirs.select { |d| pair.include?(attribute_for(d)) } raise Error.new( "Directive collision: #{}", at: offenders.last.source_location(file), ) end end |
.check_form_scope!(directives, file) ⇒ Object
Scope rules for the form gem (form-spec §8 / §10.2):
data-formis only allowed on<form>elements- multiple bare
<form>(no data-form attr) within the same component would collide on the:defaultscope, so flag the second occurrence as an error
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/lilac/directives/lints.rb', line 40 def self.check_form_scope!(directives, file) seen_default_form = false directives.each do |d| next unless d.kind == :form if d.element_tag != "form" raise Error.new( "data-form is only allowed on <form> elements " \ "(found on <#{d.element_tag}>).", at: d.source_location(file), suggestion: "Move data-form to a <form> element, or drop it if scope isn't needed.", ) end # `value == ""` is the synthetic marker injected by TemplateAST # for bare <form>. Track the first; flag any second occurrence. if d.value.to_s.empty? if seen_default_form raise Error.new( "second bare <form> in the same component would collide on the " \ ":default scope.", at: d.source_location(file), suggestion: %(Add `data-form="..."` to one of them to distinguish.), ) end seen_default_form = true end end end |
.check_gn_hidden_conflict(dirs, file) ⇒ Object
lil-hidden is reserved by data-show / data-hide. If the user
puts 'lil-hidden': @x in data-class on the same element, three
signals can race over the class — fail at build time and tell
the user to drop the data-class entry.
Note: parses the data-class hash literal purely for this check.
The substring guard above keeps the cost zero for the common
case (no lil-hidden anywhere in the value).
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/lilac/directives/lints.rb', line 102 def self.check_gn_hidden_conflict(dirs, file) return unless dirs.any? { |d| %i[show hide].include?(d.kind) } class_dir = dirs.find { |d| d.kind == :class_ } return unless class_dir return unless class_dir.value.to_s.include?("lil-hidden") pairs = begin ClassParser.parse(class_dir.value) rescue ClassParser::Error # Malformed data-class — the runtime scanner surfaces the # parse error at mount; skip the conflict check here. return end return unless pairs.any? { |key, _| key == "lil-hidden" } raise Error.new( "data-class uses the reserved class `lil-hidden` on an element " \ "that also has data-show / data-hide.", at: class_dir.source_location(file), suggestion: "Drop the `lil-hidden` key from data-class — data-show/data-hide manage it.", ) end |