@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 { BUILDERS, CALLBACK_PROP, EVENT_ATTRIBUTE, RESERVED_ENTRY_KEYS, RUNTIME_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, bare, getAttribute, getExpression, getKey, getCallbackKind, getName, getObject, getParamReadKind, getProperties, getProperty, isBound, isReservedAttribute, isStructure, ownerOf, } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['invalid_svg_expression', 'missing_remove_condition', 'invalid_remove_condition', 'duplicate_remove_condition', 'nested_binding_builder', 'callback_attribute', 'non_function_callback', 'duplicate_binding', 'invalid_bindings_object', 'removed_in_raw_bindings', 'invalid_raw_properties', 'static_binding_value', 'builder_as_bindings', 'invalid_bindings_key', 'removed_in_bindings', 'state_in_bindings', 'invalid_binding_map', 'invalid_binding_map_entry', 'unknown_visibility', 'array_binding_value', 'array_condition', 'invalid_binding_value', 'invalid_binding_placement', 'invalid_method_target', 'spread_builder_arguments', 'duplicate_builder_key', 'invalid_context_arguments', 'missing_function_argument', 'invalid_callback_target', 'invalid_placeholder', 'invalid_comparison_arguments', 'literal_comparison_binding', 'invalid_comparison_value', 'invalid_raw_bindings_type', 'insufficient_conditions', 'invalid_raw_arguments', 'call_in_raw', 'non_object_raw', 'non_literal_raw', 'invalid_builder_import', 'non_identifier_binding_name', 'invalid_binding_import', 'missing_context_name'];
3
4
  /** The converter's reserved sources (identity.ts `isReservedRegistry`) and the DM's own `document`: no fn()/ctx() names. */
4
5
  const RESERVED_FOR_BINDINGS = ['runtime', 'interactReact', 'interact', 'motionPresets', 'menu', 'document', 'core'];
5
6
  /** The key of a `bindings` object (parseJsx.ts bindingsIrOfExpression); the other two sections are written in `className` since converter 0.79. */
@@ -27,6 +28,7 @@ const PLAIN = 'a string, number or true / false';
27
28
  const RENDER_BUILDERS = ['ctx', 'fn', ...TRANSFORM_BUILDERS];
28
29
  export default defineRule({
29
30
  name: 'bindings',
31
+ checks: CHECKS,
30
32
  editors: ['studio2'],
31
33
  callbacks: {
32
34
  JSXElement: checkJSXElementBindings,
@@ -117,7 +119,7 @@ function checkSvgArguments({ node, rulesApi, report }) {
117
119
  return;
118
120
  }
119
121
  }
120
- report({
122
+ report({ check: 'invalid_svg_expression',
121
123
  loc: node.loc,
122
124
  message: tracked("An SVG media expression must be `svg(media.getShapeUrl('wix:image://v1/<uri>').url, '<uri>')`, with matching URI strings, svg imported from '@wix/origami-runtime' and media from '@wix/sdk'.", 'CONV-vectorArtExpression.ts:44'),
123
125
  });
@@ -125,20 +127,20 @@ function checkSvgArguments({ node, rulesApi, report }) {
125
127
  /** Each builder's own row: the shape of its call, a key written twice in it. */
126
128
  const ROW_OF_BUILDER = { all: 'CONV-decode.ts:196', callback: 'CONV-astValue.ts:287', ctx: 'CONV-astValue.ts:276', fn: 'CONV-astValue.ts:287', is: 'CONV-decode.ts:172', raw: 'CONV-decode.ts:28' };
127
129
  const tracked = (message, row) => `${message.slice(0, -1)} (tracker ${row}).`;
128
- const say = ({ input }, node, message, row) => input.report({ loc: node.loc, message: tracked(message, row) });
130
+ const say = ({ input }, node, check, message, row) => input.report({ check, loc: node.loc, message: tracked(message, row) });
129
131
  const textOf = ({ input }, node) => input.context.sourceCode.getText(node);
130
132
  const describeIn = (at, node) => describe(node, (part) => textOf(at, part));
131
133
  function removeWhen(at) {
132
134
  const { node } = at.input;
133
135
  const when = getAttribute(node, 'when');
134
136
  if (!when) {
135
- say(at, node.openingElement.name, `${at.owner} needs \`when={…}\`: the condition that removes the component it wraps.`, 'CONV-parseJsx.ts:1113');
137
+ say(at, node.openingElement.name, 'missing_remove_condition', `${at.owner} needs \`when={…}\`: the condition that removes the component it wraps.`, 'CONV-parseJsx.ts:1113');
136
138
  return null;
137
139
  }
138
140
  const value = getExpression(when);
139
141
  const what = when.value ? `\`${at.input.context.sourceCode.getText(when.value)}\`` : 'empty';
140
142
  if (!value)
141
- say(at, when, `\`when\` on ${at.owner} is ${what}. Write the condition in braces: \`when={…}\`.`, 'CONV-parseJsx.ts:1113');
143
+ say(at, when, 'invalid_remove_condition', `\`when\` on ${at.owner} is ${what}. Write the condition in braces: \`when={…}\`.`, 'CONV-parseJsx.ts:1113');
142
144
  return value;
143
145
  }
144
146
  function removeTarget(at) {
@@ -151,28 +153,28 @@ function removeTarget(at) {
151
153
  const item = getObject(value.arguments[0]);
152
154
  // An item of another type is the bindings' own line (checkRawBindingsType).
153
155
  if (item && stringValue(getProperty(item, 'type')?.value) === BINDINGS_LIST && setsRemoved(item)) {
154
- say(at, value, `${at.owner} wraps ${ownerOf(component)}, whose \`bindings\` are \`raw()\` of an item that already sets \`features.visibility.${REMOVED}\`. Keep only one of them.`, 'CONV-decode.ts:280');
156
+ say(at, value, 'duplicate_remove_condition', `${at.owner} wraps ${ownerOf(component)}, whose \`bindings\` are \`raw()\` of an item that already sets \`features.visibility.${REMOVED}\`. Keep only one of them.`, 'CONV-decode.ts:280');
155
157
  }
156
158
  }
157
159
  }
158
160
  function builderInPlace(at) {
159
161
  const { node, rulesApi } = at.input;
160
162
  for (const [call, where] of misplacedBuilders(node, rulesApi)) {
161
- say(at, call, `\`${builderOf(call, rulesApi)}()\` on ${at.owner} is inside ${where}, where the import reads only plain values. Write a builder as the whole value of a data attribute (\`label={ctx(…)}\`) or of an elementProps entry key (\`navbar: { label: ctx(…) }\`), or in \`bindings\`.`, 'CONV-astValue.ts:98');
163
+ say(at, call, 'nested_binding_builder', `\`${builderOf(call, rulesApi)}()\` on ${at.owner} is inside ${where}, where the import reads only plain values. Write a builder as the whole value of a data attribute (\`label={ctx(…)}\`) or of an elementProps entry key (\`navbar: { label: ctx(…) }\`), or in \`bindings\`.`, 'CONV-astValue.ts:98');
162
164
  }
163
165
  }
164
166
  /** A callback prop takes a function: the import reads it only in `bindings.props`, and there the site's runtime throws on anything but `callback()` (withOrigami). */
165
167
  function checkCallbackProps(at) {
166
168
  const { node, rulesApi } = at.input;
167
169
  for (const [name, attribute] of getCallbackAttributes(node)) {
168
- say(at, attribute, `\`${name}\` on ${at.owner} is an attribute, which the import doesn't read for a callback prop. Bind it in \`bindings\` with \`callback()\`: \`bindings={{ props: { ${name}: callback(goHome) } }}\`.`, 'OUR-031');
170
+ say(at, attribute, 'callback_attribute', `\`${name}\` on ${at.owner} is an attribute, which the import doesn't read for a callback prop. Bind it in \`bindings\` with \`callback()\`: \`bindings={{ props: { ${name}: callback(goHome) } }}\`.`, 'OUR-031');
169
171
  }
170
172
  if (!isWrappedByRuntime(node, rulesApi))
171
173
  return;
172
174
  for (const [name, value] of getBoundCallbacks(getAttribute(node, 'bindings'))) {
173
175
  const what = runsAtRender(at, value);
174
176
  if (what)
175
- say(at, value, `\`bindings.props.${name}\` on ${at.owner} is ${what}, which runs at render and gives a value, not a function: the site's runtime throws on it. Write \`${name}: callback(goHome)\`.`, 'OUR-031');
177
+ say(at, value, 'non_function_callback', `\`bindings.props.${name}\` on ${at.owner} is ${what}, which runs at render and gives a value, not a function: the site's runtime throws on it. Write \`${name}: callback(goHome)\`.`, 'OUR-031');
176
178
  }
177
179
  }
178
180
  function boundPropOnce(at) {
@@ -184,7 +186,7 @@ function boundPropOnce(at) {
184
186
  for (const [name, attribute] of boundAttributes(node, rulesApi)) {
185
187
  const where = props.raw ? `bindings={raw({ properties: { ${name} } })}` : `bindings={{ props: { ${name} } }}`;
186
188
  if (props.names.has(name))
187
- say(at, attribute, `${at.owner} binds \`${name}\` twice: in \`${name}={…}\` and in \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
189
+ say(at, attribute, 'duplicate_binding', `${at.owner} binds \`${name}\` twice: in \`${name}={…}\` and in \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
188
190
  }
189
191
  }
190
192
  function entryPropBoundOnce(at, entry) {
@@ -194,7 +196,7 @@ function entryPropBoundOnce(at, entry) {
194
196
  for (const [key, property] of boundEntryKeys(entry, at.input.rulesApi)) {
195
197
  const where = props.raw ? `bindings: raw({ properties: { ${key} } })` : `bindings.props.${key}`;
196
198
  if (props.names.has(key))
197
- say(at, property, `${at.owner} binds \`${key}\` twice: in \`${key}: …\` and in its \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
199
+ say(at, property, 'duplicate_binding', `${at.owner} binds \`${key}\` twice: in \`${key}: …\` and in its \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
198
200
  }
199
201
  }
200
202
  function bindingsAttribute(at, attribute) {
@@ -204,7 +206,7 @@ function bindingsAttribute(at, attribute) {
204
206
  if (value)
205
207
  bindingsValue(at, value);
206
208
  else
207
- say(at, attribute, notBindings(at, attribute.value ? `\`${at.input.context.sourceCode.getText(attribute.value)}\`` : 'empty'), 'CONV-parseJsx.ts:700');
209
+ say(at, attribute, 'invalid_bindings_object', notBindings(at, attribute.value ? `\`${at.input.context.sourceCode.getText(attribute.value)}\`` : 'empty'), 'CONV-parseJsx.ts:700');
208
210
  }
209
211
  const notBindings = (at, what) => `\`bindings\` on ${at.owner} is ${what}. Write an object in place, \`bindings={{ props: {…} }}\`, or \`raw({…})\`.`;
210
212
  function bindingsValue(at, node, isEntry = false) {
@@ -220,21 +222,21 @@ function bindingsValue(at, node, isEntry = false) {
220
222
  if (!isEntry)
221
223
  checkRawBindingsType(at, value, object);
222
224
  if (!isEntry && setsRemoved(object))
223
- say(at, value, `${at.owner} sets \`features.visibility.${REMOVED}\` in its \`raw()\` bindings. Move the condition out, and wrap the component in \`<Remove when={…}>\`.`, 'CONV-decode.ts:280');
225
+ say(at, value, 'removed_in_raw_bindings', `${at.owner} sets \`features.visibility.${REMOVED}\` in its \`raw()\` bindings. Move the condition out, and wrap the component in \`<Remove when={…}>\`.`, 'CONV-decode.ts:280');
224
226
  const propertyMap = getProperty(object, 'properties');
225
227
  const properties = getObject(propertyMap?.value);
226
228
  if (propertyMap && !properties)
227
- say(at, propertyMap.value, `The \`properties\` in \`raw()\` bindings on ${at.owner} isn't an object. Write a prop map of typed bindings: \`properties: { label: { type: 'ContextBinding', … } }\`.`, 'CONV-decode.ts:252');
229
+ say(at, propertyMap.value, 'invalid_raw_properties', `The \`properties\` in \`raw()\` bindings on ${at.owner} isn't an object. Write a prop map of typed bindings: \`properties: { label: { type: 'ContextBinding', … } }\`.`, 'CONV-decode.ts:252');
228
230
  for (const property of properties ? getProperties(properties) : []) {
229
231
  if (isStaticItem(property.value))
230
- say(at, property.value, fixedValue(at, getKey(property)), 'CONV-decode.ts:252');
232
+ say(at, property.value, 'static_binding_value', fixedValue(at, getKey(property)), 'CONV-decode.ts:252');
231
233
  }
232
234
  }
233
235
  else if (builder) {
234
- say(at, value, `\`bindings\` on ${at.owner} is a \`${builder}()\` call. Write an object and put the call under props, or use \`raw({…})\`.`, 'CONV-decode.ts:263');
236
+ say(at, value, 'builder_as_bindings', `\`bindings\` on ${at.owner} is a \`${builder}()\` call. Write an object and put the call under props, or use \`raw({…})\`.`, 'CONV-decode.ts:263');
235
237
  }
236
238
  else if (!isCallByName(value)) {
237
- say(at, value, notBindings(at, describeIn(at, value)), 'CONV-parseJsx.ts:700');
239
+ say(at, value, 'invalid_bindings_object', notBindings(at, describeIn(at, value)), 'CONV-parseJsx.ts:700');
238
240
  }
239
241
  }
240
242
  function bindingsObject(at, bindings, isEntry) {
@@ -246,7 +248,7 @@ function bindingsObject(at, bindings, isEntry) {
246
248
  : property.key.name === BINDINGS_KEY || CLASS_NAME_SECTIONS.includes(property.key.name) ? null
247
249
  : `the key \`${property.key.name}\``;
248
250
  if (what)
249
- say(at, property, `\`bindings\` on ${at.owner} has ${what}. Use only the key props, written as a plain name.`, 'CONV-parseJsx.ts:708');
251
+ say(at, property, 'invalid_bindings_key', `\`bindings\` on ${at.owner} has ${what}. Use only the key props, written as a plain name.`, 'CONV-parseJsx.ts:708');
250
252
  else if (getKey(property) === BINDINGS_KEY)
251
253
  section(at, BINDINGS_KEY, property.value);
252
254
  else
@@ -258,16 +260,16 @@ function classNameSection(at, property, isEntry) {
258
260
  const key = getKey(property);
259
261
  const removed = key === 'visibility' && !isEntry && getObject(property.value) && getProperty(getObject(property.value), REMOVED);
260
262
  if (removed)
261
- return say(at, removed, `${at.owner} sets \`visibility.${REMOVED}\` in its \`bindings\`. Move the condition out, and wrap the component in \`<Remove when={…}>\`.`, 'CONV-decode.ts:280');
263
+ return say(at, removed, 'removed_in_bindings', `${at.owner} sets \`visibility.${REMOVED}\` in its \`bindings\`. Move the condition out, and wrap the component in \`<Remove when={…}>\`.`, 'CONV-decode.ts:280');
262
264
  const term = key === 'states' ? '{ hover: is(…) }' : "{ 'wix-visibility-hidden': is(…) }";
263
265
  const fix = isEntry ? `in the entry's \`className: ${term}\`` : `as a \`className\` term: \`className={['comp-x', ${term}]}\``;
264
- say(at, property, `\`bindings.${key}\` on ${at.owner} isn't read: the import reads states and visibility in \`className\`. Write each condition ${fix}.`, 'CONV-parseJsx.ts:708');
266
+ say(at, property, 'state_in_bindings', `\`bindings.${key}\` on ${at.owner} isn't read: the import reads states and visibility in \`className\`. Write each condition ${fix}.`, 'CONV-parseJsx.ts:708');
265
267
  }
266
268
  function section(at, name, node) {
267
269
  const fix = `Write an object of \`key: value\` entries: \`${name}: { label: … }\`.`;
268
270
  const value = bare(node);
269
271
  if (value.type !== 'ObjectExpression')
270
- return say(at, value, `\`bindings.${name}\` on ${at.owner} isn't an object. ${fix}`, 'CONV-parseJsx.ts:669');
272
+ return say(at, value, 'invalid_binding_map', `\`bindings.${name}\` on ${at.owner} isn't an object. ${fix}`, 'CONV-parseJsx.ts:669');
271
273
  for (const property of value.properties) {
272
274
  const problem = property.type === 'SpreadElement' ? 'has a spread'
273
275
  : property.computed ? 'has a computed key'
@@ -277,7 +279,7 @@ function section(at, name, node) {
277
279
  : getKey(property) === null ? `has the key \`${textOf(at, property.key)}\``
278
280
  : null;
279
281
  if (problem) {
280
- say(at, property, `\`bindings.${name}\` on ${at.owner} ${problem}. ${fix}`, 'CONV-parseJsx.ts:669');
282
+ say(at, property, 'invalid_binding_map_entry', `\`bindings.${name}\` on ${at.owner} ${problem}. ${fix}`, 'CONV-parseJsx.ts:669');
281
283
  continue;
282
284
  }
283
285
  prop(at, getKey(property), property.value, false);
@@ -299,7 +301,7 @@ function classNameConditions(at, terms, where) {
299
301
  if (key === null)
300
302
  continue;
301
303
  if (key.startsWith(VISIBILITY_PREFIX) && !VISIBILITY_KEYS.includes(key.slice(VISIBILITY_PREFIX.length))) {
302
- say(at, property, `${where} on ${at.owner} has the term \`${key}\`, which isn't a visibility. Use \`${VISIBILITY_PREFIX}hidden\` or \`${VISIBILITY_PREFIX}collapsed\`.`, 'CONV-decode.ts:221');
304
+ say(at, property, 'unknown_visibility', `${where} on ${at.owner} has the term \`${key}\`, which isn't a visibility. Use \`${VISIBILITY_PREFIX}hidden\` or \`${VISIBILITY_PREFIX}collapsed\`.`, 'CONV-decode.ts:221');
303
305
  }
304
306
  else
305
307
  condition(at, property.value, `${where} term \`${key}\``);
@@ -308,9 +310,9 @@ function classNameConditions(at, terms, where) {
308
310
  function prop(at, name, node, placeholderAllowed) {
309
311
  const value = bare(node);
310
312
  if (value.type === 'ArrayExpression')
311
- return say(at, value, `\`bindings.props.${name}\` on ${at.owner} is an array. Bind the prop to one value: \`ctx()\`, \`fn()\` or \`raw()\`.`, 'CONV-decode.ts:209');
313
+ return say(at, value, 'array_binding_value', `\`bindings.props.${name}\` on ${at.owner} is an array. Bind the prop to one value: \`ctx()\`, \`fn()\` or \`raw()\`.`, 'CONV-decode.ts:209');
312
314
  if (isPlain(value) || rawOnly(value) || isStaticRaw(at, value) || isTyped(value, at.input.rulesApi))
313
- return say(at, value, fixedValue(at, name), 'CONV-decode.ts:252');
315
+ return say(at, value, 'static_binding_value', fixedValue(at, name), 'CONV-decode.ts:252');
314
316
  binding(at, value, 'prop', placeholderAllowed);
315
317
  }
316
318
  const fixedValue = (at, name) => `\`${name}\` on ${at.owner} is bound to a fixed value. Write it as the attribute \`${name}=…\`, or as an \`fn()\` argument.`;
@@ -326,7 +328,7 @@ function condition(at, node, where, inRemove = false) {
326
328
  return binding(at, value, 'condition');
327
329
  if (inRemove || value.elements.length > 0) {
328
330
  const spread = hasHoleOrSpread(value);
329
- say(at, value, `${where} on ${at.owner} is ${spread ? 'a list with a spread or an empty place' : 'a list'}. Write one condition, or \`all(a, b, …)\`.`, spread ? 'CONV-parseJsx.ts:655' : 'CONV-decode.ts:190');
331
+ say(at, value, 'array_condition', `${where} on ${at.owner} is ${spread ? 'a list with a spread or an empty place' : 'a list'}. Write one condition, or \`all(a, b, …)\`.`, spread ? 'CONV-parseJsx.ts:655' : 'CONV-decode.ts:190');
330
332
  }
331
333
  }
332
334
  function binding(at, node, place, placeholderAllowed = false) {
@@ -356,27 +358,27 @@ function binding(at, node, place, placeholderAllowed = false) {
356
358
  if (raw === 'an object' && place === 'fnArg' && isDataItem(value))
357
359
  return;
358
360
  if (raw === 'an object' && place === 'fnArg')
359
- return say(at, value, `${capitalize(name)} on ${at.owner} is an object that isn't a data item. Write ${PLAIN}, a link (\`{ href: … }\`) or a typed item (\`{ type: 'Builder.Image', … }\`), or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
361
+ return say(at, value, 'invalid_binding_value', `${capitalize(name)} on ${at.owner} is an object that isn't a data item. Write ${PLAIN}, a link (\`{ href: … }\`) or a typed item (\`{ type: 'Builder.Image', … }\`), or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
360
362
  if (raw)
361
- return say(at, value, `${capitalize(name)} on ${at.owner} is ${raw}. Write ${PLAIN}, or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
362
- say(at, value, `${capitalize(name)} on ${at.owner} is ${describeIn(at, value)}. Write it out. ${hintFor(place)}`, 'CONV-decode.ts:86');
363
+ return say(at, value, 'invalid_binding_value', `${capitalize(name)} on ${at.owner} is ${raw}. Write ${PLAIN}, or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
364
+ say(at, value, 'invalid_binding_value', `${capitalize(name)} on ${at.owner} is ${describeIn(at, value)}. Write it out. ${hintFor(place)}`, 'CONV-decode.ts:86');
363
365
  }
364
- const wrongPlace = (at, node, what, place) => say(at, node, `${capitalize(what)} can't be ${PLACES[place].name} on ${at.owner}. ${hintFor(place)}`, 'CONV-decode.ts:126');
366
+ const wrongPlace = (at, node, what, place) => say(at, node, 'invalid_binding_placement', `${capitalize(what)} can't be ${PLACES[place].name} on ${at.owner}. ${hintFor(place)}`, 'CONV-decode.ts:126');
365
367
  /** A called ctx() or row member: `ctx('cart.add', { from: useCart })(item.id)`, `item.actions.select('quick')` (decode.ts decodeInvoke). */
366
368
  function invoke(at, node) {
367
369
  const { callee } = node;
368
370
  if (!isMethodTarget(callee, at.input.rulesApi))
369
- return say(at, node, `${capitalize(describeIn(at, node))} on ${at.owner} calls what isn't a context or the renderItem row. Call only \`ctx(…)\` or a member of the row: \`ctx('cart.add', { from: useCart })(item.id)\`.`, 'CONV-decode.ts:86');
371
+ return say(at, node, 'invalid_method_target', `${capitalize(describeIn(at, node))} on ${at.owner} calls what isn't a context or the renderItem row. Call only \`ctx(…)\` or a member of the row: \`ctx('cart.add', { from: useCart })(item.id)\`.`, 'CONV-decode.ts:86');
370
372
  if (callee.type === 'CallExpression')
371
373
  ctxCall(at, callee, false);
372
374
  if (node.arguments.some((argument) => argument.type === 'SpreadElement'))
373
- return say(at, node, `A called \`ctx()\` or row member on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
375
+ return say(at, node, 'spread_builder_arguments', `A called \`ctx()\` or row member on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
374
376
  for (const argument of node.arguments)
375
377
  binding(at, argument, 'fnArg');
376
378
  }
377
379
  function call(at, node, builder, place, placeholderAllowed) {
378
380
  if (node.arguments.some((argument) => argument.type === 'SpreadElement')) {
379
- return say(at, node, `\`${builder}()\` on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
381
+ return say(at, node, 'spread_builder_arguments', `\`${builder}()\` on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
380
382
  }
381
383
  // map() and shape() hold a callback, read in its own scope: no tracker row checks its shape yet.
382
384
  if (builder === 'map' || builder === 'shape' || hasDuplicateKey(at, node, builder))
@@ -397,7 +399,7 @@ function call(at, node, builder, place, placeholderAllowed) {
397
399
  function hasDuplicateKey(at, node, builder) {
398
400
  const twice = node.arguments.map(duplicateKeyIn).find(Boolean);
399
401
  if (twice)
400
- say(at, twice, `\`${builder}()\` on ${at.owner} has the key \`${getKey(twice)}\` twice. Keep one.`, ROW_OF_BUILDER[builder]);
402
+ say(at, twice, 'duplicate_builder_key', `\`${builder}()\` on ${at.owner} has the key \`${getKey(twice)}\` twice. Keep one.`, ROW_OF_BUILDER[builder]);
401
403
  return Boolean(twice);
402
404
  }
403
405
  function ctxCall(at, node, placeholderAllowed) {
@@ -414,7 +416,7 @@ function ctxCall(at, node, placeholderAllowed) {
414
416
  : keys.includes('placeholder') && !placeholderAllowed ? 'has a placeholder: only a whole attribute value, or an elementProps entry value, has one'
415
417
  : null;
416
418
  if (problem)
417
- return say(at, node, `\`ctx()\` on ${at.owner} ${problem}. Write \`ctx('item.path', { from: useX })\`.`, 'CONV-astValue.ts:276');
419
+ return say(at, node, 'invalid_context_arguments', `\`ctx()\` on ${at.owner} ${problem}. Write \`ctx('item.path', { from: useX })\`.`, 'CONV-astValue.ts:276');
418
420
  const placeholder = object && getProperty(object, 'placeholder');
419
421
  if (placeholder)
420
422
  placeholderValue(at, placeholder.value);
@@ -422,7 +424,7 @@ function ctxCall(at, node, placeholderAllowed) {
422
424
  function fnCall(at, node, placeholderAllowed) {
423
425
  const [, ...rest] = node.arguments;
424
426
  if (node.arguments.length === 0)
425
- return say(at, node, `\`fn()\` on ${at.owner} has no arguments. Write the imported function first: \`fn(numberToText, …)\`.`, 'CONV-astValue.ts:287');
427
+ return say(at, node, 'missing_function_argument', `\`fn()\` on ${at.owner} has no arguments. Write the imported function first: \`fn(numberToText, …)\`.`, 'CONV-astValue.ts:287');
426
428
  const last = rest.at(-1);
427
429
  const options = placeholderAllowed && last ? getObject(last) : null;
428
430
  const placeholder = options && options.properties.length === 1 ? getProperty(options, 'placeholder') : undefined;
@@ -437,12 +439,12 @@ function fnCall(at, node, placeholderAllowed) {
437
439
  function callbackCall(at, node) {
438
440
  const [first, ...bound] = node.arguments;
439
441
  if (!first)
440
- return say(at, node, `\`callback()\` on ${at.owner} has no arguments. Write the function first: \`callback(goHome, …)\`.`, 'CONV-astValue.ts:287');
442
+ return say(at, node, 'missing_function_argument', `\`callback()\` on ${at.owner} has no arguments. Write the function first: \`callback(goHome, …)\`.`, 'CONV-astValue.ts:287');
441
443
  const target = bare(first);
442
444
  // A name that isn't a parameter is the imported function: bindingNameImported checks its import.
443
445
  const isFunctionName = target.type === 'Identifier' && !getParamReadKind(target);
444
446
  if (!isFunctionName && !isMethodTarget(target, at.input.rulesApi))
445
- return say(at, target, `\`callback()\`'s function on ${at.owner} is ${describeIn(at, target)}. Write an imported function, a \`ctx()\` or a member of the renderItem row: \`callback(goHome, …)\`.`, 'CONV-decode.ts:86');
447
+ return say(at, target, 'invalid_callback_target', `\`callback()\`'s function on ${at.owner} is ${describeIn(at, target)}. Write an imported function, a \`ctx()\` or a member of the renderItem row: \`callback(goHome, …)\`.`, 'CONV-decode.ts:86');
446
448
  if (target.type === 'CallExpression')
447
449
  ctxCall(at, target, false);
448
450
  for (const argument of bound)
@@ -451,20 +453,20 @@ function callbackCall(at, node) {
451
453
  function placeholderValue(at, node) {
452
454
  const value = bare(node);
453
455
  if (!isPlain(value))
454
- say(at, value, `The placeholder on ${at.owner} is ${rawOnly(value) ?? describeIn(at, value)}. Write ${PLAIN}.`, 'CONV-parseJsx.ts:729');
456
+ say(at, value, 'invalid_placeholder', `The placeholder on ${at.owner} is ${rawOnly(value) ?? describeIn(at, value)}. Write ${PLAIN}.`, 'CONV-parseJsx.ts:729');
455
457
  }
456
458
  function isCall(at, node) {
457
459
  const fix = 'Write `is(<ctx() or fn()>, <string or true / false>)`.';
458
460
  if (node.arguments.length !== 2)
459
- return say(at, node, `\`is()\` on ${at.owner} has ${node.arguments.length} argument${node.arguments.length === 1 ? '' : 's'}. ${fix}`, 'CONV-decode.ts:172');
461
+ return say(at, node, 'invalid_comparison_arguments', `\`is()\` on ${at.owner} has ${node.arguments.length} argument${node.arguments.length === 1 ? '' : 's'}. ${fix}`, 'CONV-decode.ts:172');
460
462
  const [binded, compared] = node.arguments.map((argument) => bare(argument));
461
463
  if (isPlain(binded) || rawOnly(binded) || builderOf(binded, at.input.rulesApi) === 'raw' || isTyped(binded, at.input.rulesApi))
462
- say(at, binded, `\`is()\` on ${at.owner} compares ${isCallByName(binded) ? `\`${builderOf(binded, at.input.rulesApi)}()\`` : 'a literal'}, not a binding. ${fix}`, 'CONV-decode.ts:172');
464
+ say(at, binded, 'literal_comparison_binding', `\`is()\` on ${at.owner} compares ${isCallByName(binded) ? `\`${builderOf(binded, at.input.rulesApi)}()\`` : 'a literal'}, not a binding. ${fix}`, 'CONV-decode.ts:172');
463
465
  else
464
466
  binding(at, binded, 'isBinding');
465
467
  const isStringOrBoolean = compared.type === 'Literal' && ['string', 'boolean'].includes(typeof compared.value);
466
468
  if (!isStringOrBoolean)
467
- say(at, compared, `\`is()\` on ${at.owner} compares to ${rawOnly(compared) ?? describeIn(at, compared)}. ${fix}`, 'CONV-decode.ts:172');
469
+ say(at, compared, 'invalid_comparison_value', `\`is()\` on ${at.owner} compares to ${rawOnly(compared) ?? describeIn(at, compared)}. ${fix}`, 'CONV-decode.ts:172');
468
470
  }
469
471
  /**
470
472
  * A component's `raw()` bindings are one item of the bindings namespace, and its one schema is BindingsList (reservedAttributes.ts
@@ -476,33 +478,33 @@ function checkRawBindingsType(at, node, item) {
476
478
  if (stringValue(type?.value) === BINDINGS_LIST)
477
479
  return;
478
480
  const has = type ? `has the type \`${textOf(at, type.value)}\`` : 'has no `type`';
479
- say(at, type?.value ?? node, `The \`raw()\` bindings item on ${at.owner} ${has}. Write \`raw({ type: '${BINDINGS_LIST}', … })\`: the import stores no other bindings item.`, 'CONV-decode.ts:28');
481
+ say(at, type?.value ?? node, 'invalid_raw_bindings_type', `The \`raw()\` bindings item on ${at.owner} ${has}. Write \`raw({ type: '${BINDINGS_LIST}', … })\`: the import stores no other bindings item.`, 'CONV-decode.ts:28');
480
482
  }
481
483
  function allCall(at, node, place) {
482
484
  const count = node.arguments.length;
483
485
  if (place === 'condition' && count < 2)
484
- say(at, node, `\`all()\` on ${at.owner} has ${count} condition${count === 1 ? '' : 's'}. Give it two or more, or write the one condition without \`all()\`.`, 'CONV-decode.ts:196');
486
+ say(at, node, 'insufficient_conditions', `\`all()\` on ${at.owner} has ${count} condition${count === 1 ? '' : 's'}. Give it two or more, or write the one condition without \`all()\`.`, 'CONV-decode.ts:196');
485
487
  for (const argument of node.arguments)
486
488
  binding(at, argument, 'allItem');
487
489
  }
488
490
  function isRawWrittenOut(at, node) {
489
491
  const [item, ...extra] = node.arguments;
490
492
  if (!item || extra.length > 0) {
491
- say(at, node, `\`raw()\` on ${at.owner} has ${node.arguments.length} arguments. Write one item: \`raw({ type: …, … })\`.`, 'CONV-decode.ts:28');
493
+ say(at, node, 'invalid_raw_arguments', `\`raw()\` on ${at.owner} has ${node.arguments.length} arguments. Write one item: \`raw({ type: …, … })\`.`, 'CONV-decode.ts:28');
492
494
  return false;
493
495
  }
494
496
  const value = bare(item);
495
497
  if (value.type === 'CallExpression') {
496
- say(at, value, `\`raw()\` on ${at.owner} wraps a call (\`${textOf(at, value)}\`). Write the item out as an object, or use the builder without \`raw()\`.`, 'CONV-decode.ts:28');
498
+ say(at, value, 'call_in_raw', `\`raw()\` on ${at.owner} wraps a call (\`${textOf(at, value)}\`). Write the item out as an object, or use the builder without \`raw()\`.`, 'CONV-decode.ts:28');
497
499
  return false;
498
500
  }
499
501
  if (value.type !== 'ObjectExpression') {
500
- say(at, value, `\`raw()\` on ${at.owner} wraps ${rawOnly(value) ?? describeIn(at, value)}, not an object. Write one item: \`raw({ type: …, … })\`.`, 'CONV-decode.ts:28');
502
+ say(at, value, 'non_object_raw', `\`raw()\` on ${at.owner} wraps ${rawOnly(value) ?? describeIn(at, value)}, not an object. Write one item: \`raw({ type: …, … })\`.`, 'CONV-decode.ts:28');
501
503
  return false;
502
504
  }
503
505
  const found = nonLiteralIn(value);
504
506
  if (found)
505
- say(at, found, `\`raw()\` on ${at.owner} holds ${describeIn(at, found)}. Write its item out with literal values.`, 'CONV-decode.ts:28');
507
+ say(at, found, 'non_literal_raw', `\`raw()\` on ${at.owner} holds ${describeIn(at, found)}. Write its item out with literal values.`, 'CONV-decode.ts:28');
506
508
  return !found;
507
509
  }
508
510
  /** Only BUILDERS: other runtime names (`Remove(…)`) pass the converter's parse, then its decode has no case for them. */
@@ -511,7 +513,7 @@ function isBuilderImported({ rulesApi, report }, callee) {
511
513
  if (found?.kind === 'runtime' && found.imported === callee.name && BUILDERS.includes(callee.name))
512
514
  return true;
513
515
  const builder = found?.kind === 'runtime' ? found.imported : callee.name;
514
- report({
516
+ report({ check: 'invalid_builder_import',
515
517
  loc: callee.loc,
516
518
  message: tracked(BUILDERS.includes(builder)
517
519
  ? `\`${callee.name}(…)\` isn't the builder \`${builder}\` from ${RUNTIME_PACKAGE}. Import it under its own name, \`import { ${builder} } from '${RUNTIME_PACKAGE}'\`, and call it as \`${builder}(…)\`.`
@@ -530,7 +532,7 @@ function bindingNameImported({ node, rulesApi, report, context }, builder) {
530
532
  const value = bare(named);
531
533
  const what = builder === 'fn' ? "fn()'s function" : builder === 'callback' ? "callback()'s function" : "ctx()'s `from` hook";
532
534
  if (value.type !== 'Identifier') {
533
- report({ loc: value.loc, message: tracked(`${what} is \`${context.sourceCode.getText(value)}\`. Write an imported name: \`fn(numberToText, …)\`, \`{ from: useProduct }\`.`, 'CONV-astValue.ts:227') });
535
+ report({ check: 'non_identifier_binding_name', loc: value.loc, message: tracked(`${what} is \`${context.sourceCode.getText(value)}\`. Write an imported name: \`fn(numberToText, …)\`, \`{ from: useProduct }\`.`, 'CONV-astValue.ts:227') });
534
536
  return null;
535
537
  }
536
538
  const found = rulesApi.getImport(value);
@@ -542,13 +544,13 @@ function bindingNameImported({ node, rulesApi, report, context }, builder) {
542
544
  ? [`${what} \`${value.name}\` is a namespace (\`import * as ${value.name}\`). Import the ${builder === 'fn' ? 'function' : 'hook'} itself by name: \`import { name } from '${found.source}'\`.`, 'CONV-astValue.ts:227']
543
545
  : null;
544
546
  if (problem)
545
- report({ loc: value.loc, message: tracked(...problem) });
547
+ report({ check: 'invalid_binding_import', loc: value.loc, message: tracked(...problem) });
546
548
  return found && !problem && builder === 'ctx' ? { hook: value, source: found.source } : null;
547
549
  }
548
550
  function ctxSourceHasName({ report }, { hook, source }) {
549
551
  if (source.indexOf('/') !== source.length - 1)
550
552
  return;
551
- report({ loc: hook.loc, message: tracked(`ctx()'s \`from\` hook \`${hook.name}\` is imported from '${source}', which has no context name after the \`/\`. Write '<componentType>/<contextName>'.`, 'CONV-decode.ts:100') });
553
+ report({ check: 'missing_context_name', loc: hook.loc, message: tracked(`ctx()'s \`from\` hook \`${hook.name}\` is imported from '${source}', which has no context name after the \`/\`. Write '<componentType>/<contextName>'.`, 'CONV-decode.ts:100') });
552
554
  }
553
555
  // ── reading the code ──
554
556
  function getElementPropsEntries(element) {
@@ -1,9 +1,11 @@
1
1
  import { describeAtRule, reportCssProblem } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['malformed_custom_media', 'duplicate_breakpoint', 'unknown_breakpoint_name', 'changed_breakpoint_range', 'undeclared_breakpoint', 'unknown_breakpoint_range'];
3
4
  /** The converter's shape for a declaration's name: a dashed identifier. */
4
5
  const DASHED_NAME = /^--[A-Za-z0-9_-]+$/;
5
6
  export default defineRule({
6
7
  name: 'breakpoints',
8
+ checks: CHECKS,
7
9
  editors: ['studio2'],
8
10
  callbacks: {
9
11
  Program: ({ rulesApi, report }) => {
@@ -34,7 +36,7 @@ function isWellFormed(file, declaration, report) {
34
36
  if (hasShape(declaration))
35
37
  return true;
36
38
  const params = `${declaration.name} ${declaration.query}`.trim();
37
- reportCssProblem(report, file.name, declaration.line, `\`@custom-media ${params}\` isn't \`--<name> <media query>\`; write it like \`@custom-media --mobile (max-width: 750px);\` (tracker CONV-astParser.ts:328).`);
39
+ reportCssProblem(report, file.name, declaration.line, 'malformed_custom_media', `\`@custom-media ${params}\` isn't \`--<name> <media query>\`; write it like \`@custom-media --mobile (max-width: 750px);\` (tracker CONV-astParser.ts:328).`);
38
40
  return false;
39
41
  }
40
42
  /** Reports a second `@custom-media` of a name the file already declares: the import can't tell two ranges with one name apart (CONV-breakpoints.ts:197). */
@@ -42,7 +44,7 @@ function isFirstOfName(file, declaration, first, report) {
42
44
  const earlier = first.get(declaration.name);
43
45
  if (!earlier)
44
46
  return first.set(declaration.name, declaration), true;
45
- reportCssProblem(report, file.name, declaration.line, `\`@custom-media ${declaration.name}\` is declared again (first at ${file.name}:${earlier.line}). The import can't tell two ranges with one name apart: keep one (tracker CONV-breakpoints.ts:197).`);
47
+ reportCssProblem(report, file.name, declaration.line, 'duplicate_breakpoint', `\`@custom-media ${declaration.name}\` is declared again (first at ${file.name}:${earlier.line}). The import can't tell two ranges with one name apart: keep one (tracker CONV-breakpoints.ts:197).`);
46
48
  return false;
47
49
  }
48
50
  /** Reports a page file's declaration whose name isn't one of the site's (CONV-astParser.ts:335). */
@@ -50,7 +52,7 @@ function isSiteName(file, declaration, site, report) {
50
52
  const names = site.breakpoints.map(({ name }) => name);
51
53
  if (names.includes(declaration.name))
52
54
  return true;
53
- reportCssProblem(report, file.name, declaration.line, `\`@custom-media ${declaration.name}\` isn't one of the site's named breakpoints (the theme has ${names.join(', ') || 'none'}). Use a range the site declares; breakpoints are added in the editor, not in CSS (tracker CONV-astParser.ts:335).`);
55
+ reportCssProblem(report, file.name, declaration.line, 'unknown_breakpoint_name', `\`@custom-media ${declaration.name}\` isn't one of the site's named breakpoints (the theme has ${names.join(', ') || 'none'}). Use a range the site declares; breakpoints are added in the editor, not in CSS (tracker CONV-astParser.ts:335).`);
54
56
  return false;
55
57
  }
56
58
  /** Reports a page file's declaration whose range isn't the site's for that name, to the letter: the import compares the text (CONV-astParser.ts:339). */
@@ -58,7 +60,7 @@ function keepsSiteRange(file, declaration, site, report) {
58
60
  const exported = getSiteRange(declaration.name, site);
59
61
  if (exported.trim() === declaration.query.trim())
60
62
  return true;
61
- reportCssProblem(report, file.name, declaration.line, `\`@custom-media ${declaration.name} ${declaration.query}\` changes the site's range for ${declaration.name}, which is \`${exported}\`. Restore it; a breakpoint's width is changed in the editor, not in CSS (tracker CONV-astParser.ts:339).`);
63
+ reportCssProblem(report, file.name, declaration.line, 'changed_breakpoint_range', `\`@custom-media ${declaration.name} ${declaration.query}\` changes the site's range for ${declaration.name}, which is \`${exported}\`. Restore it; a breakpoint's width is changed in the editor, not in CSS (tracker CONV-astParser.ts:339).`);
62
64
  return false;
63
65
  }
64
66
  /** Reports a `@media (--name)` with no declaration of that name in the same file (CONV-breakpoints.ts:57). A malformed declaration of the name (its own line) counts as the declaration, so the use gets no second line. */
@@ -71,7 +73,7 @@ function checkNamedUses(file, site, report) {
71
73
  const fix = range
72
74
  ? `add \`@custom-media ${name} ${range};\` at the top of ${file.name}, or write the range itself, \`@media ${range}\``
73
75
  : `add \`@custom-media ${name} (<its range>);\` at the top of ${file.name} (the range the site gives it), or write the range itself`;
74
- reportCssProblem(report, file.name, line, `\`@media (${name})\` uses a name the file doesn't declare: ${fix} (tracker CONV-breakpoints.ts:57).`);
76
+ reportCssProblem(report, file.name, line, 'undeclared_breakpoint', `\`@media (${name})\` uses a name the file doesn't declare: ${fix} (tracker CONV-breakpoints.ts:57).`);
75
77
  }
76
78
  }
77
79
  /** Reports each `@media` of a file whose range is none of the site's (P4-R01, CONV-breakpoints.ts:75); nothing without a theme to compare with. A name is read through the file's declaration when that declaration is the site's; otherwise the steps above report it. A component's own breakpoint (inside `@scope`) is compared once unmapped.json is read. */
@@ -84,7 +86,7 @@ function checkRanges(file, site, report) {
84
86
  const query = resolve(media, file.declared, site);
85
87
  if (query === null || site.breakpoints.some((breakpoint) => canonical(breakpoint.query) === canonical(query)))
86
88
  continue;
87
- reportCssProblem(report, file.name, media.line, `\`${describeAtRule({ name: 'media', params: media.query })}\` is a breakpoint the site doesn't have. Write one of the theme's breakpoints: ${describe(site.breakpoints)} (rule P4-R01; tracker CONV-breakpoints.ts:75). The import refuses a value for a breakpoint the site doesn't have.`);
89
+ reportCssProblem(report, file.name, media.line, 'unknown_breakpoint_range', `\`${describeAtRule({ name: 'media', params: media.query })}\` is a breakpoint the site doesn't have. Write one of the theme's breakpoints: ${describe(site.breakpoints)} (rule P4-R01; tracker CONV-breakpoints.ts:75). The import refuses a value for a breakpoint the site doesn't have.`);
88
90
  }
89
91
  }
90
92
  // ── reading the files ──
@@ -1,9 +1,11 @@
1
1
  import { BREAKPOINT_AT_RULES, createCssFileReporter, describeAtRule, getCssChildren, ORIGAMI_TARGETS, PREAMBLE_LAYER } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['css_import', 'unsupported_top_level_at_rule', 'top_level_declaration', 'invalid_scope_content', 'at_rule_in_breakpoint', 'declaration_in_breakpoint', 'missing_at_rule_block', 'invalid_layout_rule_content', 'invalid_target_content', 'unknown_target', 'duplicate_target', 'at_rule_in_style_rule'];
3
4
  /** The import's leading-import line (cssFileParser.ts `IMPORT_LINE_PATTERN`). */
4
5
  const IMPORT_LINE = /^@import '([^']+)';$/;
5
6
  export default defineRule({
6
7
  name: 'css-file-nesting',
8
+ checks: CHECKS,
7
9
  editors: ['studio2'],
8
10
  callbacks: {
9
11
  Program: ({ rulesApi, report }) => {
@@ -35,7 +37,7 @@ function checkTopLevel(root, file, report) {
35
37
  }
36
38
  function checkTopAtRule(node, file, isLeadingImport, reportInFile) {
37
39
  if (node.name === 'import' && isLeadingImport)
38
- return reportInFile(node, `\`@import ${node.params.trim()}\` can't be in ${file}: an item's CSS carries no imports (the preview loads the theme from _generated/). Remove the line (tracker CONV-fromBundle.ts:987).`);
40
+ return reportInFile(node, 'css_import', `\`@import ${node.params.trim()}\` can't be in ${file}: an item's CSS carries no imports (the preview loads the theme from _generated/). Remove the line (tracker CONV-fromBundle.ts:987).`);
39
41
  if (node.name === 'custom-media')
40
42
  return;
41
43
  if (node.name === 'mixin' && file !== 'layout.css')
@@ -46,19 +48,19 @@ function checkTopAtRule(node, file, isLeadingImport, reportInFile) {
46
48
  return;
47
49
  if (BREAKPOINT_AT_RULES.includes(node.name))
48
50
  return checkBreakpoint(node, file, reportInFile);
49
- reportInFile(node, `\`@${node.name}\` isn't part of the export format at the top of ${file}. At the top level only @media, @scope, @custom-media${file === 'layout.css' ? '' : ' and @mixin'} are allowed${pageMixinNote(file)} (tracker CONV-utils.ts:92).`);
51
+ reportInFile(node, 'unsupported_top_level_at_rule', `\`@${node.name}\` isn't part of the export format at the top of ${file}. At the top level only @media, @scope, @custom-media${file === 'layout.css' ? '' : ' and @mixin'} are allowed${pageMixinNote(file)} (tracker CONV-utils.ts:92).`);
50
52
  }
51
53
  /** Reports a bare declaration at the top of layout.css (CONV-astParser.ts:412); style.css and theme.css drop one silently. */
52
54
  function checkTopLevelDeclaration(node, file, reportInFile) {
53
55
  if (file !== 'layout.css')
54
56
  return;
55
- reportInFile(node, `\`${node.prop}: ${node.value}\` is at the top level. Put the declaration inside the element's rule (\`.comp-a { … }\`) (tracker CONV-astParser.ts:412).`);
57
+ reportInFile(node, 'top_level_declaration', `\`${node.prop}: ${node.value}\` is at the top level. Put the declaration inside the element's rule (\`.comp-a { … }\`) (tracker CONV-astParser.ts:412).`);
56
58
  }
57
59
  /** A layout `@scope` holds exactly one `@media`, checked before its block (CONV-astParser.ts:399, as parseCssAst does). */
58
60
  function checkLayoutScope(node, reportInFile) {
59
61
  const scopeChildren = node.nodes === undefined ? [] : getCssChildren(node);
60
62
  if (scopeChildren.length !== 1 || !isMedia(scopeChildren[0])) {
61
- return reportInFile(node, `\`@scope ${node.params.trim()}\` must hold exactly one \`@media (…) { … }\` block. Put the rules inside one @media in the @scope (tracker CONV-astParser.ts:399).`);
63
+ return reportInFile(node, 'invalid_scope_content', `\`@scope ${node.params.trim()}\` must hold exactly one \`@media (…) { … }\` block. Put the rules inside one @media in the @scope (tracker CONV-astParser.ts:399).`);
62
64
  }
63
65
  if (hasBlock(scopeChildren[0], reportInFile))
64
66
  checkBreakpointContent(scopeChildren[0], 'layout.css', reportInFile);
@@ -81,7 +83,7 @@ function getScopedMedia(node, reportInFile) {
81
83
  continue;
82
84
  }
83
85
  if (!isMedia(first)) {
84
- reportInFile(current, `\`@scope ${current.params.trim()}\` must start with a \`@media (…) { … }\` block: put the rules inside it (tracker CONV-utils.ts:92).`);
86
+ reportInFile(current, 'invalid_scope_content', `\`@scope ${current.params.trim()}\` must start with a \`@media (…) { … }\` block: put the rules inside it (tracker CONV-utils.ts:92).`);
85
87
  return null;
86
88
  }
87
89
  if (!hasBlock(first, reportInFile))
@@ -96,16 +98,16 @@ function checkBreakpointContent(media, file, reportInFile) {
96
98
  if (child.type === 'rule')
97
99
  checkRule(child, file, reportInFile);
98
100
  else if (child.type === 'atrule')
99
- reportInFile(child, `\`@${child.name}\` inside \`${describeAtRule(media)}\` is not allowed. A breakpoint block holds only element rules: declare \`@mixin\` at the top level${pageMixinNote(file)} and put its breakpoint variant in a \`@media\` inside its \`@result\`; don't nest one \`@media\` in another (tracker CONV-astParser.ts:147).`);
101
+ reportInFile(child, 'at_rule_in_breakpoint', `\`@${child.name}\` inside \`${describeAtRule(media)}\` is not allowed. A breakpoint block holds only element rules: declare \`@mixin\` at the top level${pageMixinNote(file)} and put its breakpoint variant in a \`@media\` inside its \`@result\`; don't nest one \`@media\` in another (tracker CONV-astParser.ts:147).`);
100
102
  else if (child.type === 'decl')
101
- reportInFile(child, `\`${child.prop}: ${child.value}\` sits directly in \`${describeAtRule(media)}\`. Wrap it in the element's rule: \`@media (…) { .comp-a { … } }\` (tracker CONV-astParser.ts:151).`);
103
+ reportInFile(child, 'declaration_in_breakpoint', `\`${child.prop}: ${child.value}\` sits directly in \`${describeAtRule(media)}\`. Wrap it in the element's rule: \`@media (…) { .comp-a { … } }\` (tracker CONV-astParser.ts:151).`);
102
104
  }
103
105
  }
104
106
  /** Reports an at-rule written without a block (CONV-utils.ts:76): the import reads every top-level at-rule but `@custom-media` and `@mixin` as a breakpoint. */
105
107
  function hasBlock(node, reportInFile) {
106
108
  if (node.nodes !== undefined)
107
109
  return true;
108
- reportInFile(node, `\`${describeAtRule(node)}\` has no block. Give it a \`{ … }\` block with the element rules, or remove it (tracker CONV-utils.ts:76).`);
110
+ reportInFile(node, 'missing_at_rule_block', `\`${describeAtRule(node)}\` has no block. Give it a \`{ … }\` block with the element rules, or remove it (tracker CONV-utils.ts:76).`);
109
111
  return false;
110
112
  }
111
113
  /** An element rule: layout's (CONV-astParser.ts:74, :51, :54, :57) or style's and theme's (CONV-astParser.ts:95). */
@@ -127,19 +129,19 @@ function checkLayoutRule(rule, reportInFile) {
127
129
  }
128
130
  const what = child.type === 'rule' ? `the rule \`${child.selector.trim()}\`` : `\`${describeAtRule(child)}\``;
129
131
  const fix = child.type === 'rule' ? 'Write it as its own rule' : child.name === 'media' ? `Move it out: \`@media (…) { ${selector} { … } }\` at the top level` : 'Move it out of the rule';
130
- reportInFile(child, `rule \`${selector}\`: ${what} can't be inside a layout rule, which holds only declarations and @origami-target blocks. ${fix} (tracker CONV-astParser.ts:74).`);
132
+ reportInFile(child, 'invalid_layout_rule_content', `rule \`${selector}\`: ${what} can't be inside a layout rule, which holds only declarations and @origami-target blocks. ${fix} (tracker CONV-astParser.ts:74).`);
131
133
  }
132
134
  }
133
135
  /** An `@origami-target`: a block (CONV-astParser.ts:51), a known name (:54), once per rule (:57), only declarations (:51), in the converter's order. */
134
136
  function checkOrigamiTarget(node, selector, seenTargets, reportInFile) {
135
137
  const target = node.params.trim();
136
- const reportTargetShape = (fix) => reportInFile(node, `rule \`${selector}\`: \`@origami-target ${target}\` holds only declarations, in a \`{ … }\` block. ${fix} (tracker CONV-astParser.ts:51).`);
138
+ const reportTargetShape = (fix) => reportInFile(node, 'invalid_target_content', `rule \`${selector}\`: \`@origami-target ${target}\` holds only declarations, in a \`{ … }\` block. ${fix} (tracker CONV-astParser.ts:51).`);
137
139
  if (node.nodes === undefined)
138
140
  return reportTargetShape("Give it a block with the container's declarations");
139
141
  if (!ORIGAMI_TARGETS.includes(target))
140
- return reportInFile(node, `rule \`${selector}\`: \`@origami-target ${target}\` is not a known target. Use \`@origami-target ${ORIGAMI_TARGETS[0]}\` or \`@origami-target ${ORIGAMI_TARGETS[1]}\` (tracker CONV-astParser.ts:54).`);
142
+ return reportInFile(node, 'unknown_target', `rule \`${selector}\`: \`@origami-target ${target}\` is not a known target. Use \`@origami-target ${ORIGAMI_TARGETS[0]}\` or \`@origami-target ${ORIGAMI_TARGETS[1]}\` (tracker CONV-astParser.ts:54).`);
141
143
  if (seenTargets.has(target))
142
- return reportInFile(node, `rule \`${selector}\`: \`@origami-target ${target}\` appears twice. Merge the two blocks into one (tracker CONV-astParser.ts:57).`);
144
+ return reportInFile(node, 'duplicate_target', `rule \`${selector}\`: \`@origami-target ${target}\` appears twice. Merge the two blocks into one (tracker CONV-astParser.ts:57).`);
143
145
  seenTargets.add(target);
144
146
  const nested = getCssChildren(node).find((child) => child.type !== 'decl');
145
147
  if (!nested)
@@ -151,7 +153,7 @@ function checkStyleRule(rule, reportInFile) {
151
153
  const selector = rule.selector.trim();
152
154
  for (const child of getCssChildren(rule)) {
153
155
  if (child.type === 'atrule' && child.name !== 'apply') {
154
- reportInFile(child, `rule \`${selector}\`: \`@${child.name}\` can't be nested in a style rule. Put the rule inside it instead: \`@media (…) { ${selector} { … } }\` (in a @mixin: \`@result { @media (…) { &… { … } } }\`) (tracker CONV-astParser.ts:95).`);
156
+ reportInFile(child, 'at_rule_in_style_rule', `rule \`${selector}\`: \`@${child.name}\` can't be nested in a style rule. Put the rule inside it instead: \`@media (…) { ${selector} { … } }\` (in a @mixin: \`@result { @media (…) { &… { … } } }\`) (tracker CONV-astParser.ts:95).`);
155
157
  }
156
158
  }
157
159
  }