@wix/origami-lint 1.34.19 → 1.34.21

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.
@@ -1,5 +1,6 @@
1
1
  import { CONTENT_TARGET, createCssFileReporter, getName, LAYER_ATTRIBUTES, OVERFLOW_TARGET } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['duplicate_property', 'non_layout_property', 'property_outside_target', 'property_in_wrong_target', 'non_container_target_property', 'missing_property_value', 'incompatible_container_property', 'invalid_layout_value', 'missing_container_display', 'missing_grid_column', 'invalid_spx_context', 'unsupported_flex_wrap', 'child_without_container_layout'];
3
4
  // ── the tables (propSpecs.ts) ──
4
5
  const keywords = (...values) => ({ values, allowed: values.join(', ') });
5
6
  const ANY = { allowed: 'any value' };
@@ -88,6 +89,7 @@ const isLayoutProperty = (prop) => isRootProperty(prop) || prop in CONTAINER_PRO
88
89
  const ROW = (line) => `(tracker CONV-layoutParser.ts:${line})`;
89
90
  export default defineRule({
90
91
  name: 'css-layout-properties',
92
+ checks: CHECKS,
91
93
  editors: ['studio2'],
92
94
  callbacks: {
93
95
  Program: checkLayoutFile,
@@ -127,14 +129,14 @@ function checkPropertiesOnce(declarations, where, reportAt) {
127
129
  const seen = new Set();
128
130
  for (const decl of declarations) {
129
131
  if (seen.has(decl.prop))
130
- reportAt(decl, `\`${decl.prop}\` is declared twice in ${where}: keep one ${ROW(498)}.`);
132
+ reportAt(decl, 'duplicate_property', `\`${decl.prop}\` is declared twice in ${where}: keep one ${ROW(498)}.`);
131
133
  seen.add(decl.prop);
132
134
  }
133
135
  }
134
136
  function checkLayoutPropertiesOnly({ element, declarations }, reportAt) {
135
137
  for (const decl of declarations) {
136
138
  if (!isLayoutProperty(decl.prop))
137
- reportAt(decl, `${describeNonLayoutProperty(decl.prop, element)} ${ROW(505)}.`);
139
+ reportAt(decl, 'non_layout_property', `${describeNonLayoutProperty(decl.prop, element)} ${ROW(505)}.`);
138
140
  }
139
141
  }
140
142
  /** A property the layout tables don't have: in capitals, the import doesn't know it (it reads names as written), else it is paint for style.css. */
@@ -150,9 +152,9 @@ function checkRootPropertiesAtTop({ element, declarations }, reportAt) {
150
152
  continue;
151
153
  const container = CONTAINER_PROPS[decl.prop];
152
154
  if (container)
153
- reportAt(decl, `${element}'s \`${decl.prop}\` goes inside \`@origami-target ${container.target} { … }\`, not at the top of its rule ${ROW(512)}.`);
155
+ reportAt(decl, 'property_outside_target', `${element}'s \`${decl.prop}\` goes inside \`@origami-target ${container.target} { … }\`, not at the top of its rule ${ROW(512)}.`);
154
156
  else
155
- reportAt(decl, `${describeNonLayoutProperty(decl.prop, element)} ${ROW(512)}.`);
157
+ reportAt(decl, 'non_layout_property', `${describeNonLayoutProperty(decl.prop, element)} ${ROW(512)}.`);
156
158
  }
157
159
  }
158
160
  /** Each declaration of a target block is a container property of that target; the content block also takes `position: relative` (targetContract.ts). */
@@ -165,11 +167,11 @@ function checkTargetProperties(element, target, declarations, reportAt) {
165
167
  continue;
166
168
  if (container) {
167
169
  const place = container.target === 'root' ? `at the top of ${element}'s rule` : `in \`@origami-target ${container.target}\``;
168
- reportAt(decl, `${element}'s \`${decl.prop}\` goes ${place}, not in ${target} ${ROW(524)}.`);
170
+ reportAt(decl, 'property_in_wrong_target', `${element}'s \`${decl.prop}\` goes ${place}, not in ${target} ${ROW(524)}.`);
169
171
  }
170
172
  else {
171
173
  const fix = isLayoutProperty(decl.prop) ? `move it to the top of ${element}'s rule` : `move it to ${element}'s rule in style.css`;
172
- reportAt(decl, `\`${decl.prop}\` in ${element}'s \`@origami-target ${target}\` block is not a container property: ${fix} ${ROW(524)}.`);
174
+ reportAt(decl, 'non_container_target_property', `\`${decl.prop}\` in ${element}'s \`@origami-target ${target}\` block is not a container property: ${fix} ${ROW(524)}.`);
173
175
  }
174
176
  }
175
177
  }
@@ -182,7 +184,7 @@ function checkScope(scope, inherited, reportAt) {
182
184
  for (const [prop, decl] of values) {
183
185
  const value = decl.value.trim();
184
186
  if (value === '') {
185
- reportAt(decl, `${element}'s \`${prop}\` has no value: write a value or remove the declaration ${ROW(430)}.`);
187
+ reportAt(decl, 'missing_property_value', `${element}'s \`${prop}\` has no value: write a value or remove the declaration ${ROW(430)}.`);
186
188
  continue;
187
189
  }
188
190
  if (SPX_PROPS.includes(prop))
@@ -190,21 +192,21 @@ function checkScope(scope, inherited, reportAt) {
190
192
  const container = CONTAINER_PROPS[prop];
191
193
  if (container && type && !container[type]) {
192
194
  const [own, other] = type === 'grid' ? ['grid', 'flex'] : ['flex', 'grid'];
193
- reportAt(decl, `${element} is a ${own} container (\`display: ${own}\`), and \`${prop}\` is a ${other} property: remove it, or use \`display: ${other}\` ${ROW(447)}.`);
195
+ reportAt(decl, 'incompatible_container_property', `${element} is a ${own} container (\`display: ${own}\`), and \`${prop}\` is a ${other} property: remove it, or use \`display: ${other}\` ${ROW(447)}.`);
194
196
  continue;
195
197
  }
196
198
  if (prop === 'flex-wrap' && ownType === 'stack')
197
199
  continue; // :218 reported it; an inherited flex reads it as a value (:437)
198
200
  const spec = ELEMENT_PROPS[prop] ?? ITEM_PROPS[prop] ?? container?.spec;
199
201
  if (spec && !isReadable(spec, value))
200
- reportAt(decl, `${element}'s \`${prop}: ${value}\` is not a layout value. Allowed: ${spec.allowed} ${ROW(437)}.`);
202
+ reportAt(decl, 'invalid_layout_value', `${element}'s \`${prop}: ${value}\` is not a layout value. Allowed: ${spec.allowed} ${ROW(437)}.`);
201
203
  }
202
204
  if (scope.rulesWithTargets.length && !type) {
203
- reportAt(scope.rulesWithTargets[0], `${element} has \`@origami-target\` blocks but no display: add \`display: grid\` or \`display: flex\` in its \`@origami-target ${CONTENT_TARGET}\` block ${ROW(451)}.`);
205
+ reportAt(scope.rulesWithTargets[0], 'missing_container_display', `${element} has \`@origami-target\` blocks but no display: add \`display: grid\` or \`display: flex\` in its \`@origami-target ${CONTENT_TARGET}\` block ${ROW(451)}.`);
204
206
  }
205
207
  const gridRow = values.get('grid-row');
206
208
  if (gridRow && !values.has('grid-column')) {
207
- reportAt(gridRow, `${element} has \`grid-row\` but no \`grid-column\`: add \`grid-column: <start> / <end>\` (in the same @media, if the grid-row is in one) ${ROW(264)}.`);
209
+ reportAt(gridRow, 'missing_grid_column', `${element} has \`grid-row\` but no \`grid-column\`: add \`grid-column: <start> / <end>\` (in the same @media, if the grid-row is in one) ${ROW(264)}.`);
208
210
  }
209
211
  return type;
210
212
  }
@@ -220,7 +222,7 @@ function checkSpxReference({ element, values }, reportAt) {
220
222
  return; // :430 reports the empty one
221
223
  if ((refWidthValue === undefined || Number.isFinite(Number(refWidthValue))) && (resolverTypeValue === undefined || SPX_RESOLVER_TYPES.includes(resolverTypeValue)))
222
224
  return;
223
- reportAt(first, `${element}'s spx context is invalid: \`--spx-ref-width\` takes a plain number (1280, no px) and \`--spx-resolver-type\` takes scale or fixed ${ROW(417)}.`);
225
+ reportAt(first, 'invalid_spx_context', `${element}'s spx context is invalid: \`--spx-ref-width\` takes a plain number (1280, no px) and \`--spx-resolver-type\` takes scale or fixed ${ROW(417)}.`);
224
226
  }
225
227
  /** The container type the scope's own display gives (resolveContainerType): grid, or flex with no flex-wrap other than nowrap (:218). */
226
228
  function checkContainerType({ element, values }, reportAt) {
@@ -232,7 +234,7 @@ function checkContainerType({ element, values }, reportAt) {
232
234
  const wrap = values.get('flex-wrap');
233
235
  const wrapValue = wrap?.value.trim();
234
236
  if (wrap && wrapValue !== '' && wrapValue !== 'nowrap') {
235
- reportAt(wrap, `${element} has \`flex-wrap: ${wrapValue}\`, which isn't supported: remove it (or write \`nowrap\`), or use \`display: grid\` for items that wrap ${ROW(218)}.`);
237
+ reportAt(wrap, 'unsupported_flex_wrap', `${element} has \`flex-wrap: ${wrapValue}\`, which isn't supported: remove it (or write \`nowrap\`), or use \`display: grid\` for items that wrap ${ROW(218)}.`);
236
238
  }
237
239
  return 'stack';
238
240
  }
@@ -268,7 +270,7 @@ function checkContainerParent(child, parent, { report }) {
268
270
  const parentName = nameOf(parent);
269
271
  const props = [...new Set(declarations.map(({ prop }) => prop))].join(', ');
270
272
  const line = Math.min(...declarations.map(({ decl }) => decl.line));
271
- report({ loc: child.loc, message: `layout.css:${line}: ${childName} sets item properties (${props}), but its parent ${parentName} has no container layout${media ? ` at @media ${media}` : ''}: give ${parentName} \`display: grid\` or \`display: flex\`, or remove ${childName}'s item properties ${ROW(359)}.` });
273
+ report({ check: 'child_without_container_layout', loc: child.loc, message: `layout.css:${line}: ${childName} sets item properties (${props}), but its parent ${parentName} has no container layout${media ? ` at @media ${media}` : ''}: give ${parentName} \`display: grid\` or \`display: flex\`, or remove ${childName}'s item properties ${ROW(359)}.` });
272
274
  }
273
275
  }
274
276
  /** The parent's container type at this breakpoint, as the import derives it: its last `display` there (on itself or in its content-wrapper block) when it is grid or flex, else the default's. */
@@ -1,5 +1,6 @@
1
1
  import { BREAKPOINT_AT_RULES, createCssFileReporter, getSelectorSegments, isCssIdentifier, isSafeCssIdentifier, PREAMBLE_LAYER, readSegment } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['invalid_scope_owner', 'empty_selector', 'missing_element_selector', 'global_style_as_rule', 'invalid_selector_segment', 'multiple_layout_elements', 'layout_by_state', 'too_many_ancestor_elements', 'ancestor_without_state', 'inner_part_has_id', 'parent_selector_outside_mixin', 'multiple_state_owners', 'state_without_class', 'missing_selector_element', 'selector_list', 'unclosed_selector_token', 'unsupported_selector_syntax'];
3
4
  /** The at-rules whose rules the import reads: breakpoint blocks, and a `@mixin`'s `@result`. */
4
5
  const READ_AT_RULES = [...BREAKPOINT_AT_RULES, 'result'];
5
6
  const SCOPE_OWNER = /^\(\.([^:[\]"\s]+)\)$/;
@@ -7,6 +8,7 @@ const SCOPE_OWNER = /^\(\.([^:[\]"\s]+)\)$/;
7
8
  const CLASSES_ONLY = /^[^:[\]"]+$/;
8
9
  export default defineRule({
9
10
  name: 'css-selectors',
11
+ checks: CHECKS,
10
12
  editors: ['studio2'],
11
13
  callbacks: {
12
14
  Program: checkCssFilesSelectors,
@@ -50,7 +52,7 @@ function checkScopeOwner(node, file) {
50
52
  const owner = SCOPE_OWNER.exec(params)?.[1];
51
53
  if (owner !== undefined && isSafeCssIdentifier(owner))
52
54
  return;
53
- file.reportAt(node, `\`@scope ${params}\` must name its owner as \`(.<class>)\`, one element's class. Write \`@scope (.${ownClassOf(owner ?? params.replace(/^[(.#\s]+/, ''))})\` (tracker CONV-utils.ts:70).`);
55
+ file.reportAt(node, 'invalid_scope_owner', `\`@scope ${params}\` must name its owner as \`(.<class>)\`, one element's class. Write \`@scope (.${ownClassOf(owner ?? params.replace(/^[(.#\s]+/, ''))})\` (tracker CONV-utils.ts:70).`);
54
56
  }
55
57
  /** One rule's selector, in the import's order: the flat global style, the split, the parts, the first part, then the chain. */
56
58
  function checkSelector(rule, file) {
@@ -64,13 +66,13 @@ function checkSelector(rule, file) {
64
66
  return;
65
67
  const [first, ...rest] = parts;
66
68
  if (first === undefined)
67
- return file.reportAt(rule, 'the rule has an empty selector. Start it with the element\'s class: `.comp-a { … }` (tracker CONV-cssSelectors.ts:370).');
69
+ return file.reportAt(rule, 'empty_selector', 'the rule has an empty selector. Start it with the element\'s class: `.comp-a { … }` (tracker CONV-cssSelectors.ts:370).');
68
70
  if (first.isAmpersand)
69
71
  return checkGlobalStyleChain(first, rest, selector, rule, file);
70
72
  if (file.inMixin)
71
73
  return;
72
74
  if (first.className === undefined)
73
- return file.reportAt(rule, `selector \`${selector}\` doesn't start with an element. Start it with the element's class: \`.comp-a${first.text}\` (tracker CONV-cssSelectors.ts:370).`);
75
+ return file.reportAt(rule, 'missing_element_selector', `selector \`${selector}\` doesn't start with an element. Start it with the element's class: \`.comp-a${first.text}\` (tracker CONV-cssSelectors.ts:370).`);
74
76
  const element = splitStateClasses(first);
75
77
  if (!checkElementClass(element.className, first.className, selector, rule, file))
76
78
  return;
@@ -82,7 +84,7 @@ function checkSelector(rule, file) {
82
84
  function checkFlatGlobalStyle(selector, rule, file) {
83
85
  if (file.name === 'layout.css' || file.inMixin || !selector.startsWith('--'))
84
86
  return true;
85
- file.reportAt(rule, `\`${selector} { … }\` is not a valid rule. Declare the global style as \`@mixin ${selector} { @result { … } }\` (tracker CONV-astParser.ts:88).`);
87
+ file.reportAt(rule, 'global_style_as_rule', `\`${selector} { … }\` is not a valid rule. Declare the global style as \`@mixin ${selector} { @result { … } }\` (tracker CONV-astParser.ts:88).`);
86
88
  return false;
87
89
  }
88
90
  /** Reports an element class that isn't a plain identifier or is a reserved name (CONV-cssSelectors.ts:54), naming the class as written. */
@@ -90,7 +92,7 @@ function checkElementClass(className, written, selector, rule, file) {
90
92
  if (isSafeCssIdentifier(className))
91
93
  return true;
92
94
  const problem = isCssIdentifier(className) ? `\`${className}\` is a reserved name, not an element class. Give the element another className` : `\`${written}\` is not an element class. ${howToWrite(written, ownClassOf(className))}`;
93
- file.reportAt(rule, `in \`${selector}\`, ${problem} (tracker CONV-cssSelectors.ts:54).`);
95
+ file.reportAt(rule, 'invalid_selector_segment', `in \`${selector}\`, ${problem} (tracker CONV-cssSelectors.ts:54).`);
94
96
  return false;
95
97
  }
96
98
  /** A layout.css rule: one element's class (CONV-layoutParser.ts:105), no state (:108); a part with no class is refused first (CONV-cssSelectors.ts:213). */
@@ -100,10 +102,10 @@ function checkLayoutChain(first, rest, selector, rule, file) {
100
102
  const state = [first, ...rest].flatMap((part) => part.states)[0];
101
103
  if (rest.length) {
102
104
  const element = rest.findLast((part) => file.elementClassNames.has(baseClassOf(part))) ?? first;
103
- return file.reportAt(rule, `\`${selector}\` names more than one element; a layout rule takes one element's class: write \`.${baseClassOf(element)} { … }\` (layout can't be set on an inner part or through an ancestor) (tracker CONV-layoutParser.ts:105).`);
105
+ return file.reportAt(rule, 'multiple_layout_elements', `\`${selector}\` names more than one element; a layout rule takes one element's class: write \`.${baseClassOf(element)} { … }\` (layout can't be set on an inner part or through an ancestor) (tracker CONV-layoutParser.ts:105).`);
104
106
  }
105
107
  if (state !== undefined)
106
- return file.reportAt(rule, `\`${selector}\` sets layout by state, which isn't supported: remove \`${state}\` from the selector (state changes go in style.css) (tracker CONV-layoutParser.ts:108).`);
108
+ return file.reportAt(rule, 'layout_by_state', `\`${selector}\` sets layout by state, which isn't supported: remove \`${state}\` from the selector (state changes go in style.css) (tracker CONV-layoutParser.ts:108).`);
107
109
  checkElementExists(first, rule, file);
108
110
  }
109
111
  /** A style or theme rule on an element (cssSelectors.ts parseComponentChain): a second element needs a state on the first (:321), carries
@@ -116,9 +118,9 @@ function checkElementChain(first, rest, selector, rule, file) {
116
118
  const second = splitStateClasses(rest[0]);
117
119
  const third = rest.slice(1).find((part) => isAnotherElement(part, file));
118
120
  if (third)
119
- return file.reportAt(rule, `\`${selector}\` chains three elements. Keep one ancestor: \`.${first.className}${first.states.join('')} .${baseClassOf(third)}\` (tracker CONV-cssSelectors.ts:318).`);
121
+ return file.reportAt(rule, 'too_many_ancestor_elements', `\`${selector}\` chains three elements. Keep one ancestor: \`.${first.className}${first.states.join('')} .${baseClassOf(third)}\` (tracker CONV-cssSelectors.ts:318).`);
120
122
  if (!first.states.length)
121
- return file.reportAt(rule, `\`${selector}\` puts one element under another without a state. Address the element by its own class (\`.${ownClassOf(second.className)}\`), or condition it on the ancestor's state (\`.${first.className}:hover .${ownClassOf(second.className)}\`) (tracker CONV-cssSelectors.ts:321).`);
123
+ return file.reportAt(rule, 'ancestor_without_state', `\`${selector}\` puts one element under another without a state. Address the element by its own class (\`.${ownClassOf(second.className)}\`), or condition it on the ancestor's state (\`.${first.className}:hover .${ownClassOf(second.className)}\`) (tracker CONV-cssSelectors.ts:321).`);
122
124
  if (second.states.length)
123
125
  return reportTwoOwners(first, second, selector, rule, file);
124
126
  if (!checkElementClass(second.className, rest[0].className, selector, rule, file))
@@ -147,14 +149,14 @@ function checkInnerPartsHaveNoId(inner, selector, rule, file) {
147
149
  const part = inner.find(hasId);
148
150
  if (part === undefined)
149
151
  return true;
150
- file.reportAt(rule, `in \`${selector}\`, \`.${part.className}\` is not an inner part: an inner part's selector has no \`#\` id. Use an inner part's selector from the element's contract, without the id (\`.${ownClassOf(part.className)}\`) (tracker CONV-cssSelectors.ts:222).`);
152
+ file.reportAt(rule, 'inner_part_has_id', `in \`${selector}\`, \`.${part.className}\` is not an inner part: an inner part's selector has no \`#\` id. Use an inner part's selector from the element's contract, without the id (\`.${ownClassOf(part.className)}\`) (tracker CONV-cssSelectors.ts:222).`);
151
153
  return false;
152
154
  }
153
155
  /** A `@mixin`'s rule, `&…` (cssSelectors.ts parseGlobalStyleChain): outside a `@mixin` it's refused (:377); a later `&` or `:root` is the
154
156
  * global-styles rows'; then the inner parts, read as an element's (parseIeChain), with no id (:222). */
155
157
  function checkGlobalStyleChain(first, rest, selector, rule, file) {
156
158
  if (!file.inMixin)
157
- return file.reportAt(rule, `\`${selector}\` is outside a \`@mixin\`. Replace \`&\` with the element's class (\`.comp-a${selector.slice(1)}\`), or move the rule into the global style's \`@mixin\` (tracker CONV-cssSelectors.ts:377).`);
159
+ return file.reportAt(rule, 'parent_selector_outside_mixin', `\`${selector}\` is outside a \`@mixin\`. Replace \`&\` with the element's class (\`.comp-a${selector.slice(1)}\`), or move the rule into the global style's \`@mixin\` (tracker CONV-cssSelectors.ts:377).`);
158
160
  if (rest.some((part) => part.isAmpersand || part.text === ':root'))
159
161
  return;
160
162
  if (!checkInnerParts(rest, first, selector, rule, file))
@@ -169,7 +171,7 @@ function checkInnerParts(inner, owner, selector, rule, file) {
169
171
  return false;
170
172
  const stated = inner.filter((part) => part.states.length);
171
173
  if (stated.length > 1) {
172
- file.reportAt(rule, `in \`${selector}\`, states sit on \`${stated[0].text}\` and \`${stated[1].text}\`. Attach the states to the one part that owns them (tracker CONV-cssSelectors.ts:213).`);
174
+ file.reportAt(rule, 'multiple_state_owners', `in \`${selector}\`, states sit on \`${stated[0].text}\` and \`${stated[1].text}\`. Attach the states to the one part that owns them (tracker CONV-cssSelectors.ts:213).`);
173
175
  return false;
174
176
  }
175
177
  if (stated.length && owner.states.length) {
@@ -183,19 +185,19 @@ function checkPartsHaveClass(parts, selector, rule, file) {
183
185
  const bare = parts.find((part) => part.className === undefined);
184
186
  if (!bare)
185
187
  return true;
186
- file.reportAt(rule, `in \`${selector}\`, \`${bare.text}\` has no class. Attach the state to the part that owns it (\`.comp-a:hover\`, \`.comp-a .label:hover\`) and write every later part as a class (tracker CONV-cssSelectors.ts:213).`);
188
+ file.reportAt(rule, 'state_without_class', `in \`${selector}\`, \`${bare.text}\` has no class. Attach the state to the part that owns it (\`.comp-a:hover\`, \`.comp-a .label:hover\`) and write every later part as a class (tracker CONV-cssSelectors.ts:213).`);
187
189
  return false;
188
190
  }
189
191
  /** Reports states on two owners (CONV-cssSelectors.ts:216). */
190
192
  function reportTwoOwners(owner, other, selector, rule, file) {
191
193
  const name = (part) => (part.isAmpersand ? '&' : `.${baseClassOf(part)}`);
192
- file.reportAt(rule, `\`${selector}\` puts states on two owners (\`${name(owner)}\` and \`${name(other)}\`). Condition the rule on one owner's states only (tracker CONV-cssSelectors.ts:216).`);
194
+ file.reportAt(rule, 'multiple_state_owners', `\`${selector}\` puts states on two owners (\`${name(owner)}\` and \`${name(other)}\`). Condition the rule on one owner's states only (tracker CONV-cssSelectors.ts:216).`);
193
195
  }
194
196
  /** Reports a rule for an element that structure.jsx doesn't have (CONV-fromBundle.ts:855). */
195
197
  function checkElementExists(element, rule, file) {
196
198
  if (file.inMixin || file.elementClassNames.has(element.className))
197
199
  return;
198
- file.reportAt(rule, `\`.${element.className}\` styles no element: no element in structure.jsx has className="${element.className}". Remove the rule, or write the className of an element (tracker CONV-fromBundle.ts:855).`);
200
+ file.reportAt(rule, 'missing_selector_element', `\`.${element.className}\` styles no element: no element in structure.jsx has className="${element.className}". Remove the rule, or write the className of an element (tracker CONV-fromBundle.ts:855).`);
199
201
  }
200
202
  // ── reading a selector ──
201
203
  /** The selector's parts, split as the import splits them (getSelectorSegments, cssSelectors.ts splitSegments), each read as the import
@@ -227,14 +229,14 @@ function reportUnsplittable(selector, rule, file) {
227
229
  else if ((char === ']' || char === ')') && !openers.pop())
228
230
  return reportUnbalanced(`has a \`${char}\` with no \`${char === ']' ? '[' : '('}\` before it`, selector, rule, file);
229
231
  else if (char === ',' && !openers.length) {
230
- file.reportAt(rule, `\`${selector}\` is a selector list. Split it into one rule per selector (tracker CONV-cssSelectors.ts:92).`);
232
+ file.reportAt(rule, 'selector_list', `\`${selector}\` is a selector list. Split it into one rule per selector (tracker CONV-cssSelectors.ts:92).`);
231
233
  return null;
232
234
  }
233
235
  }
234
236
  return reportUnbalanced(inQuotes ? 'has an unclosed `"`' : `has an unclosed \`${openers.at(-1)}\``, selector, rule, file);
235
237
  }
236
238
  function reportUnbalanced(what, selector, rule, file) {
237
- file.reportAt(rule, `selector \`${selector}\` ${what}. Close the bracket or quote (tracker CONV-cssSelectors.ts:87).`);
239
+ file.reportAt(rule, 'unclosed_selector_token', `selector \`${selector}\` ${what}. Close the bracket or quote (tracker CONV-cssSelectors.ts:87).`);
238
240
  return null;
239
241
  }
240
242
  /** One part as the import reads it: `:root`, or `&` or a class, then states (`:hover`, `.state-class`); an attribute or functional
@@ -244,7 +246,7 @@ function readPart(text, selector, rule, file) {
244
246
  return { text, isAmpersand: false, states: [] };
245
247
  const { base, states, unread } = readSegment(text);
246
248
  if (unread) {
247
- file.reportAt(rule, `in \`${selector}\`, the format can't read \`${unread}\`. ${howToWrite(unread, 'comp-a')} (tracker CONV-cssSelectors.ts:169).`);
249
+ file.reportAt(rule, 'unsupported_selector_syntax', `in \`${selector}\`, the format can't read \`${unread}\`. ${howToWrite(unread, 'comp-a')} (tracker CONV-cssSelectors.ts:169).`);
248
250
  return null;
249
251
  }
250
252
  return { text, isAmpersand: base === '&', className: base === '&' ? undefined : base?.slice(1), states };
@@ -4,6 +4,7 @@
4
4
  import { isDeepStrictEqual } from 'node:util';
5
5
  import { DATA_KEY, EVENT_ATTRIBUTE, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getProperty, isBound, isOneEditAway, isReservedAttribute, isStructure, ownerOf, } from "../../rules-api/index.js";
6
6
  import { defineRule } from "./define-rule.js";
7
+ const CHECKS = ['attribute_without_braces', 'non_literal_data', 'duplicate_element_prop', 'invalid_element_props_object', 'invalid_element_props_entry', 'invalid_data_object', 'invalid_element_props_key', 'unsupported_element_props_key', 'mixed_data_wrapper', 'duplicate_data_key', 'bare_document_href', 'missing_document_id', 'inconsistent_image_url', 'misplaced_image_url_call'];
7
8
  /** The names the import never reads as data (dataKeys.ts). The React name is joined as the converter does: Wix Standards fails the build on it in any file. */
8
9
  const EXCLUDED_DATA_KEYS = [
9
10
  '_slotsId', '_mapId', 'hidden', 'class', 'title', 'role', 'tabindex', 'draggable', 'lang', 'dir', 'accesskey', 'contenteditable', 'spellcheck',
@@ -33,6 +34,7 @@ const WRAPPER_KEYS = ['type', 'id', 'data'];
33
34
  const LITERAL_HINT = 'The import reads it as data: write the value itself, a string, a number, true / false, null, or an object or array of these.';
34
35
  export default defineRule({
35
36
  name: 'data-values',
37
+ checks: CHECKS,
36
38
  editors: ['studio2'],
37
39
  callbacks: {
38
40
  JSXElement: checkJSXElementDataValues,
@@ -54,7 +56,7 @@ function checkJSXElementDataValues(input) {
54
56
  checkImageUrls(input, read);
55
57
  }
56
58
  // ── the steps ──
57
- const say = ({ report }, node, message) => report({ loc: node.loc, message });
59
+ const say = ({ report }, node, check, message) => report({ check, loc: node.loc, message });
58
60
  const textOf = ({ context }) => (node) => context.sourceCode.getText(node);
59
61
  const owner = ({ node }) => ownerOf(node);
60
62
  function checkValuesAreLiterals(input, skip) {
@@ -62,7 +64,7 @@ function checkValuesAreLiterals(input, skip) {
62
64
  for (const attribute of getAttributes(input.node)) {
63
65
  const name = getName(attribute);
64
66
  if (READ_AS_DATA.includes(name) && attribute.value?.type !== 'JSXExpressionContainer') {
65
- say(input, attribute, `\`${name}\` on ${owner(input)} isn't in braces. The import reads it only from braces: write \`${name}={…}\`.`);
67
+ say(input, attribute, 'attribute_without_braces', `\`${name}\` on ${owner(input)} isn't in braces. The import reads it only from braces: write \`${name}={…}\`.`);
66
68
  continue;
67
69
  }
68
70
  const value = getExpression(attribute);
@@ -73,7 +75,7 @@ function checkValuesAreLiterals(input, skip) {
73
75
  const mode = name === 'props' ? PROPS : FULL;
74
76
  const found = name === 'elementProps' ? nonLiteralInElementProps(value, mode, input.rulesApi, textOf(input)) : nonLiteralIn(value, mode, input.rulesApi, textOf(input));
75
77
  if (found)
76
- say(input, found.node, `\`${name}\` on ${owner(input)} holds ${found.what}. ${LITERAL_HINT}`);
78
+ say(input, found.node, 'non_literal_data', `\`${name}\` on ${owner(input)} holds ${found.what}. ${LITERAL_HINT}`);
77
79
  else
78
80
  read.add(name);
79
81
  }
@@ -87,7 +89,7 @@ function checkEntryKeysOnce(input, attribute) {
87
89
  if (key === null)
88
90
  continue;
89
91
  if (seen.has(key)) {
90
- say(input, property, `${owner(input)}'s elementProps entry \`${path.join('.')}\` has the key \`${key}\` twice. Keep one.`);
92
+ say(input, property, 'duplicate_element_prop', `${owner(input)}'s elementProps entry \`${path.join('.')}\` has the key \`${key}\` twice. Keep one.`);
91
93
  return false;
92
94
  }
93
95
  seen.add(key);
@@ -100,7 +102,7 @@ function checkElementPropsShape(input, attribute) {
100
102
  // an object written out: a media call decodes to an object, but its keys aren't inner parts
101
103
  const map = getObject(value);
102
104
  if (!map)
103
- return say(input, value, `\`elementProps\` on ${owner(input)} isn't an object. Write an object of inner parts: \`elementProps={{ name: {…} }}\`.`);
105
+ return say(input, value, 'invalid_element_props_object', `\`elementProps\` on ${owner(input)} isn't an object. Write an object of inner parts: \`elementProps={{ name: {…} }}\`.`);
104
106
  checkElementPropsEntries(input, map, []);
105
107
  }
106
108
  function checkElementPropsEntries(input, map, path) {
@@ -112,19 +114,19 @@ function checkElementPropsEntries(input, map, path) {
112
114
  // an object written out: a media call decodes to an object, but not to an inner part's settings
113
115
  const entry = getObject(property.value);
114
116
  if (!entry) {
115
- say(input, property.value, `${owner(input)}'s elementProps entry \`${at}\` isn't an object. Write the inner part's settings as an object: \`${name}: {…}\`.`);
117
+ say(input, property.value, 'invalid_element_props_entry', `${owner(input)}'s elementProps entry \`${at}\` isn't an object. Write the inner part's settings as an object: \`${name}: {…}\`.`);
116
118
  continue;
117
119
  }
118
120
  checkEntryKeysAreKnown(input, entry, at);
119
121
  const data = getProperty(entry, 'data');
120
122
  if (data && !isObjectValue(data.value, input.rulesApi))
121
- say(input, data.value, `${owner(input)}'s elementProps entry \`${at}\` has \`data\` that isn't an object. Write \`data: {…}\`.`);
123
+ say(input, data.value, 'invalid_data_object', `${owner(input)}'s elementProps entry \`${at}\` has \`data\` that isn't an object. Write \`data: {…}\`.`);
122
124
  const nested = getProperty(entry, 'elementProps');
123
125
  if (!nested)
124
126
  continue;
125
127
  const nestedMap = getObject(nested.value);
126
128
  if (!nestedMap)
127
- say(input, nested.value, `${owner(input)}'s elementProps entry \`${at}\` has \`elementProps\` that isn't an object. Write \`elementProps: { name: {…} }\`.`);
129
+ say(input, nested.value, 'invalid_element_props_object', `${owner(input)}'s elementProps entry \`${at}\` has \`elementProps\` that isn't an object. Write \`elementProps: { name: {…} }\`.`);
128
130
  else
129
131
  checkElementPropsEntries(input, nestedMap, [...path, name]);
130
132
  }
@@ -132,7 +134,7 @@ function checkElementPropsEntries(input, map, path) {
132
134
  function checkEntryName(input, property, name) {
133
135
  const what = name === '' ? 'an empty name' : name.includes('|') ? `the name \`${name}\`, which has a \`|\`` : !CSS_IDENTIFIER.test(name) || RESERVED_OBJECT_KEYS.includes(name) ? `the name \`${name}\`, which isn't a CSS identifier` : null;
134
136
  if (what)
135
- say(input, property.key, `${owner(input)} has an elementProps entry with ${what}. Name it as the inner part is named, like \`navbar\`.`);
137
+ say(input, property.key, 'invalid_element_props_key', `${owner(input)} has an elementProps entry with ${what}. Name it as the inner part is named, like \`navbar\`.`);
136
138
  return what === null;
137
139
  }
138
140
  function checkEntryKeysAreKnown(input, entry, at) {
@@ -143,7 +145,7 @@ function checkEntryKeysAreKnown(input, entry, at) {
143
145
  const typo = nearMissOf(key);
144
146
  if (!isReservedAttribute(key) && !isExcluded(key) && !typo)
145
147
  continue;
146
- say(input, property.key, `${owner(input)}'s elementProps entry \`${at}\` has the key \`${key}\`, which an inner part doesn't take. Use its data keys, or \`data: {…}\`${typo ? ` (did you mean \`${typo}\`?)` : ''}.`);
148
+ say(input, property.key, 'unsupported_element_props_key', `${owner(input)}'s elementProps entry \`${at}\` has the key \`${key}\`, which an inner part doesn't take. Use its data keys, or \`data: {…}\`${typo ? ` (did you mean \`${typo}\`?)` : ''}.`);
147
149
  }
148
150
  }
149
151
  function checkDataCarrier(input) {
@@ -155,12 +157,12 @@ function checkDataCarrier(input) {
155
157
  if (!merges)
156
158
  return null;
157
159
  if (!isObjectValue(value, input.rulesApi)) {
158
- say(input, value, `${owner(input)} has data attributes, so its \`data\` must be an object: \`data={{…}}\`, not \`data={${textOf(input)(value)}}\`.`);
160
+ say(input, value, 'invalid_data_object', `${owner(input)} has data attributes, so its \`data\` must be an object: \`data={{…}}\`, not \`data={${textOf(input)(value)}}\`.`);
159
161
  return null;
160
162
  }
161
163
  const carrier = getObject(value);
162
164
  if (carrier && isWrapper(input, carrier)) {
163
- say(input, data, `${owner(input)} mixes data attributes with the \`{ type, data }\` wrapper in \`data={…}\`. Move the wrapper's keys to attributes.`);
165
+ say(input, data, 'mixed_data_wrapper', `${owner(input)} mixes data attributes with the \`{ type, data }\` wrapper in \`data={…}\`. Move the wrapper's keys to attributes.`);
164
166
  return null;
165
167
  }
166
168
  return carrier;
@@ -182,7 +184,7 @@ function checkDataKeyOnce(input, carrier) {
182
184
  for (const attribute of attributes) {
183
185
  const name = getName(attribute);
184
186
  if (keys.has(name))
185
- say(input, attribute, `${owner(input)} has \`${name}\` both as an attribute and inside \`data={{…}}\`. Keep only one.`);
187
+ say(input, attribute, 'duplicate_data_key', `${owner(input)} has \`${name}\` both as an attribute and inside \`data={{…}}\`. Keep only one.`);
186
188
  }
187
189
  }
188
190
  for (const { entry, path } of getEntries(getObject(getExpression(getAttribute(input.node, 'elementProps'))))) {
@@ -193,7 +195,7 @@ function checkDataKeyOnce(input, carrier) {
193
195
  for (const property of entryDataKeys(entry)) {
194
196
  const key = getKey(property);
195
197
  if (keys.has(key))
196
- say(input, property, `${owner(input)}'s elementProps entry \`${path.join('.')}\` has \`${key}\` both as a key and inside its \`data\`. Keep only one.`);
198
+ say(input, property, 'duplicate_data_key', `${owner(input)}'s elementProps entry \`${path.join('.')}\` has \`${key}\` both as a key and inside its \`data\`. Keep only one.`);
197
199
  }
198
200
  }
199
201
  }
@@ -230,12 +232,12 @@ function checkDocumentHref(input, link) {
230
232
  const bareHref = valueOf(href.value);
231
233
  if (typeof bareHref === 'string') {
232
234
  if (bareHref.startsWith(DOCUMENT_PREFIX))
233
- say(input, href, `${owner(input)}'s link href is a bare 'wix:document://' string. Write \`href: media.getDocumentUrl('wix:document://…').url\`, with media imported from '@wix/sdk'.`);
235
+ say(input, href, 'bare_document_href', `${owner(input)}'s link href is a bare 'wix:document://' string. Write \`href: media.getDocumentUrl('wix:document://…').url\`, with media imported from '@wix/sdk'.`);
234
236
  return;
235
237
  }
236
238
  const identifier = documentIdentifierOf(bare(href.value), input.rulesApi);
237
239
  if (!hasDocumentId(identifier))
238
- say(input, href, `${owner(input)}'s document link has no document id in '${identifier}'. Use the identifier the export wrote: \`media.getDocumentUrl('wix:document://v1/ugd/<file>/<name>').url\`.`);
240
+ say(input, href, 'missing_document_id', `${owner(input)}'s document link has no document id in '${identifier}'. Use the identifier the export wrote: \`media.getDocumentUrl('wix:document://v1/ugd/<file>/<name>').url\`.`);
239
241
  }
240
242
  /** documentLink.ts rebuildDocumentLink: the first `/ugd/` or `/archives/` goes, then `<id>/<name>`; the name is URI-decoded. */
241
243
  function hasDocumentId(identifier) {
@@ -265,12 +267,12 @@ function checkImageUrls(input, read) {
265
267
  const call = bare(url.value);
266
268
  if (isImageCall(call, input.rulesApi) && sameArguments(call, derivedImageArguments(fields)))
267
269
  continue;
268
- say(input, url, `${owner(input)}'s \`${at}.url\` isn't the url the image's fields give. It is derived: change the image's uri, width, height, name or crop, and drop \`url\`.`);
270
+ say(input, url, 'inconsistent_image_url', `${owner(input)}'s \`${at}.url\` isn't the url the image's fields give. It is derived: change the image's uri, width, height, name or crop, and drop \`url\`.`);
269
271
  }
270
272
  }
271
273
  const stray = imageCallsIn(input, values, read).find((call) => !urls.has(call));
272
274
  if (stray)
273
- say(input, stray, `${owner(input)} has \`media.getCroppedImageUrl(…)\` outside an image's url. Write it only as the \`url\` of a \`{ type: 'Builder.Image', … }\` object.`);
275
+ say(input, stray, 'misplaced_image_url_call', `${owner(input)} has \`media.getCroppedImageUrl(…)\` outside an image's url. Write it only as the \`url\` of a \`{ type: 'Builder.Image', … }\` object.`);
274
276
  }
275
277
  function imageCallsIn(input, dataValuesRead, read) {
276
278
  const values = dataValuesRead.map(([value]) => value);
@@ -1,7 +1,9 @@
1
1
  import { BREAKPOINT_AT_RULES, createCssFileReporter, describeAtRule, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isSafeCssIdentifier } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['invalid_mixin_id', 'invalid_mixin_result', 'missing_parent_selector', 'bare_parent_selector', 'nested_style_rule', 'apply_at_result_root', 'nested_mixin', 'invalid_result_at_rule', 'invalid_parent_selector', 'conditional_global_style_link', 'invalid_linked_style_id', 'missing_linked_style', 'duplicate_global_style_link', 'link_with_values', 'missing_global_style_link'];
3
4
  export default defineRule({
4
5
  name: 'global-styles',
6
+ checks: CHECKS,
5
7
  editors: ['studio2'],
6
8
  callbacks: {
7
9
  Program: ({ rulesApi, report }) => {
@@ -28,12 +30,12 @@ function checkMixinsAndLinks(root, file, report) {
28
30
  function checkMixin(mixin, reportInFile) {
29
31
  const id = getMixinId(mixin);
30
32
  if (id === null) {
31
- reportInFile(mixin, getMixinIdMessage(mixin.params.trim()));
33
+ reportInFile(mixin, 'invalid_mixin_id', getMixinIdMessage(mixin.params.trim()));
32
34
  return [];
33
35
  }
34
36
  const result = getResult(mixin);
35
37
  if (!result) {
36
- reportInFile(mixin, `\`@mixin --${id}\` must be \`@mixin --${id} { @result { … } }\`. Put everything inside one \`@result { }\` (tracker CONV-astParser.ts:293).`);
38
+ reportInFile(mixin, 'invalid_mixin_result', `\`@mixin --${id}\` must be \`@mixin --${id} { @result { … } }\`. Put everything inside one \`@result { }\` (tracker CONV-astParser.ts:293).`);
37
39
  return [];
38
40
  }
39
41
  const rules = [];
@@ -54,12 +56,12 @@ function checkMixinRule(node, id, breakpoint, rules, reportInFile) {
54
56
  const selector = node.selector.trim();
55
57
  const inner = getCssChildren(node);
56
58
  if (!selector.startsWith('&'))
57
- return reportInFile(node, `\`@mixin --${id}\`: selector \`${selector}\` must start with \`&\`. Write \`& ${selector}\` (or \`&:hover\`) (tracker CONV-astParser.ts:195).`);
59
+ return reportInFile(node, 'missing_parent_selector', `\`@mixin --${id}\`: selector \`${selector}\` must start with \`&\`. Write \`& ${selector}\` (or \`&:hover\`) (tracker CONV-astParser.ts:195).`);
58
60
  if (selector === '&')
59
- return reportInFile(node, getBareAmpersandMessage(id, inner));
61
+ return reportInFile(node, 'bare_parent_selector', getBareAmpersandMessage(id, inner));
60
62
  const nested = inner.find((child) => child.type === 'rule');
61
63
  if (nested)
62
- return reportInFile(nested, `\`@mixin --${id}\`: a rule is nested in \`${selector}\`. Write the whole selector on one rule: \`${joinSelectors(selector, nested.selector.trim())} { … }\` (tracker CONV-astParser.ts:204).`);
64
+ return reportInFile(nested, 'nested_style_rule', `\`@mixin --${id}\`: a rule is nested in \`${selector}\`. Write the whole selector on one rule: \`${joinSelectors(selector, nested.selector.trim())} { … }\` (tracker CONV-astParser.ts:204).`);
63
65
  const atRules = inner.filter((child) => child.type === 'atrule');
64
66
  const applies = atRules.filter((child) => child.name === 'apply');
65
67
  // another at-rule (CONV-astParser.ts:211) is css-file-nesting's; two `@apply`s, one beside declarations, or one that isn't
@@ -71,12 +73,12 @@ function checkMixinRule(node, id, breakpoint, rules, reportInFile) {
71
73
  /** An at-rule in `@result`: no `@apply` (:246), no `@mixin` (:251); only `@media` / `@scope` at its top, none inside them (:254). */
72
74
  function checkResultAtRule(node, id, breakpoint, rules, reportInFile) {
73
75
  if (node.name === 'apply')
74
- return reportInFile(node, `\`@mixin --${id}\`: \`@apply\` can't sit at the root of \`@result\`. Remove it, or move it into an inner part's rule: \`& .label { @apply ${node.params.trim()}; }\` (tracker CONV-astParser.ts:246).`);
76
+ return reportInFile(node, 'apply_at_result_root', `\`@mixin --${id}\`: \`@apply\` can't sit at the root of \`@result\`. Remove it, or move it into an inner part's rule: \`& .label { @apply ${node.params.trim()}; }\` (tracker CONV-astParser.ts:246).`);
75
77
  if (node.name === 'mixin')
76
- return reportInFile(node, `\`@mixin --${id}\`: \`@mixin ${node.params.trim()}\` is declared inside it. Declare each \`@mixin\` at the top level of the file (tracker CONV-astParser.ts:251).`);
78
+ return reportInFile(node, 'nested_mixin', `\`@mixin --${id}\`: \`@mixin ${node.params.trim()}\` is declared inside it. Declare each \`@mixin\` at the top level of the file (tracker CONV-astParser.ts:251).`);
77
79
  if (breakpoint || !BREAKPOINT_AT_RULES.includes(node.name)) {
78
80
  const where = breakpoint ? `\`${describeAtRule(breakpoint)}\` in \`@result\`` : '`@result`';
79
- return reportInFile(node, `\`@mixin --${id}\`: \`@${node.name}\` is not allowed in ${where}. Use only \`@media\` / \`@scope\` directly inside \`@result\`, one level deep (tracker CONV-astParser.ts:254).`);
81
+ return reportInFile(node, 'invalid_result_at_rule', `\`@mixin --${id}\`: \`@${node.name}\` is not allowed in ${where}. Use only \`@media\` / \`@scope\` directly inside \`@result\`, one level deep (tracker CONV-astParser.ts:254).`);
80
82
  }
81
83
  const media = getBreakpointBody(node);
82
84
  if (media)
@@ -98,7 +100,7 @@ function checkSelector(rule, reportInFile) {
98
100
  if (repeated === undefined)
99
101
  return true;
100
102
  const problem = repeated === ':root' ? 'has `:root` after `&`' : 'repeats `&`';
101
- reportInFile(rule.node, `\`@mixin --${rule.mixin}\`: selector \`${rule.selector}\` ${problem}. Write \`&\` once, at the start of the selector (tracker CONV-cssSelectors.ts:351).`);
103
+ reportInFile(rule.node, 'invalid_parent_selector', `\`@mixin --${rule.mixin}\`: selector \`${rule.selector}\` ${problem}. Write \`&\` once, at the start of the selector (tracker CONV-cssSelectors.ts:351).`);
102
104
  return false;
103
105
  }
104
106
  /** Each link, in the import's order (collectGlobalStyleLinks): no state (:655), no breakpoint (:664), a dashed id (:670) the file declares (:673), once per part (:677). Returns the parts a link names, by mixin. */
@@ -110,26 +112,26 @@ function checkLinks(links, declaredIds, reportInFile) {
110
112
  const linkDescription = `\`@mixin --${link.mixin}\`'s link \`@apply ${child}\``;
111
113
  const fix = `write it as \`${getSelectorWithoutStates(link.selector)} { @apply ${child} }\``;
112
114
  if (hasState(link.selector)) {
113
- reportInFile(link.apply, `${linkDescription} can't be under a state or a breakpoint: ${fix} and put the state / breakpoint values in @mixin ${child} (tracker CONV-styleParser.ts:655).`);
115
+ reportInFile(link.apply, 'conditional_global_style_link', `${linkDescription} can't be under a state or a breakpoint: ${fix} and put the state / breakpoint values in @mixin ${child} (tracker CONV-styleParser.ts:655).`);
114
116
  continue;
115
117
  }
116
118
  if (link.breakpoint) {
117
- reportInFile(link.apply, `${linkDescription} can't be under a state or a breakpoint: ${fix} outside \`${describeAtRule(link.breakpoint)}\`, and put the breakpoint values in @mixin ${child} (tracker CONV-styleParser.ts:655).`);
119
+ reportInFile(link.apply, 'conditional_global_style_link', `${linkDescription} can't be under a state or a breakpoint: ${fix} outside \`${describeAtRule(link.breakpoint)}\`, and put the breakpoint values in @mixin ${child} (tracker CONV-styleParser.ts:655).`);
118
120
  continue;
119
121
  }
120
122
  const part = getPartKey(link);
121
123
  linkedParts.add(part);
122
124
  const id = getDashedId(child);
123
125
  if (!isSafeCssIdentifier(id)) {
124
- reportInFile(link.apply, `\`@mixin --${link.mixin}\` links \`${child}\`, which is not a valid global style name: use letters, digits, - and _ (tracker CONV-styleParser.ts:670).`);
126
+ reportInFile(link.apply, 'invalid_linked_style_id', `\`@mixin --${link.mixin}\` links \`${child}\`, which is not a valid global style name: use letters, digits, - and _ (tracker CONV-styleParser.ts:670).`);
125
127
  continue;
126
128
  }
127
129
  if (!declaredIds.has(id)) {
128
- reportInFile(link.apply, `\`@mixin --${link.mixin}\` links \`${child}\`, but the file has no \`@mixin ${child}\`: add it (tracker CONV-styleParser.ts:673).`);
130
+ reportInFile(link.apply, 'missing_linked_style', `\`@mixin --${link.mixin}\` links \`${child}\`, but the file has no \`@mixin ${child}\`: add it (tracker CONV-styleParser.ts:673).`);
129
131
  continue;
130
132
  }
131
133
  if (validLinkParts.has(part)) {
132
- reportInFile(link.apply, `\`@mixin --${link.mixin}\` links two global styles at \`${getPart(link.selector)}\`: keep one (tracker CONV-styleParser.ts:677).`);
134
+ reportInFile(link.apply, 'duplicate_global_style_link', `\`@mixin --${link.mixin}\` links two global styles at \`${getPart(link.selector)}\`: keep one (tracker CONV-styleParser.ts:677).`);
133
135
  continue;
134
136
  }
135
137
  validLinkParts.add(part);
@@ -142,12 +144,12 @@ function checkLinkOrValues(group, linkedParts, reportInFile) {
142
144
  const values = group.filter((rule) => !rule.apply);
143
145
  const [first] = group;
144
146
  if (link && values.length > 0) {
145
- return reportInFile(values[0].node, `\`${first.selector}\` of \`@mixin --${first.mixin}\` has both \`@apply ${link.apply.params.trim()}\` and values: move the values into @mixin ${link.apply.params.trim()} (tracker CONV-styleParser.ts:803).`);
147
+ return reportInFile(values[0].node, 'link_with_values', `\`${first.selector}\` of \`@mixin --${first.mixin}\` has both \`@apply ${link.apply.params.trim()}\` and values: move the values into @mixin ${link.apply.params.trim()} (tracker CONV-styleParser.ts:803).`);
146
148
  }
147
149
  const part = getPart(first.selector);
148
150
  if (link || part === '' || linkedParts.has(getPartKey(first)))
149
151
  return;
150
- reportInFile(first.node, `\`@mixin --${first.mixin}\` styles \`${part}\` but links no global style there: add \`& ${part} { @apply --<child> }\` and move the values to that child's @mixin (tracker CONV-styleParser.ts:698).`);
152
+ reportInFile(first.node, 'missing_global_style_link', `\`@mixin --${first.mixin}\` styles \`${part}\` but links no global style there: add \`& ${part} { @apply --<child> }\` and move the values to that child's @mixin (tracker CONV-styleParser.ts:698).`);
151
153
  }
152
154
  // ── reading a mixin ──
153
155
  /** The mixin's one `@result` with no params and a block, beside nothing; undefined otherwise. */
@@ -1,7 +1,9 @@
1
1
  import { SECTIONS } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['absolute_section'];
3
4
  export default defineRule({
4
5
  name: 'no-absolute-section',
6
+ checks: CHECKS,
5
7
  editors: 'all',
6
8
  callbacks: {
7
9
  JSXElement: (input) => {
@@ -20,7 +22,7 @@ function getSection({ node, rulesApi }) {
20
22
  /** Reports each `position: absolute` the section has, in any CSS, on any part of it, at any @media or @origami-target. */
21
23
  function hasNoAbsolutePosition(section, { rulesApi, report }) {
22
24
  for (const decl of rulesApi.getCss(section, 'position').filter(isAbsolute)) {
23
- report({ loc: section.loc, message: `Section ${section.key} has \`position: absolute\`${describeWhere(decl)}. Sections can't be absolutely positioned.` });
25
+ report({ check: 'absolute_section', loc: section.loc, message: `Section ${section.key} has \`position: absolute\`${describeWhere(decl)}. Sections can't be absolutely positioned.` });
24
26
  }
25
27
  }
26
28
  // ── reporting ──