@wix/origami-lint 1.34.27 → 1.34.29

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.
@@ -119,28 +119,29 @@ function checkSvgArguments({ node, rulesApi, report }) {
119
119
  return;
120
120
  }
121
121
  }
122
+ // tracker CONV-vectorArtExpression.ts:44
122
123
  report({ check: 'invalid_svg_expression',
123
124
  loc: node.loc,
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'),
125
+ message: "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'.",
125
126
  });
126
127
  }
127
- /** Each builder's own row: the shape of its call, a key written twice in it. */
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' };
129
- const tracked = (message, row) => `${message.slice(0, -1)} (tracker ${row}).`;
130
- const say = ({ input }, node, check, message, row) => input.report({ check, loc: node.loc, message: tracked(message, row) });
128
+ // ── the steps ──
129
+ const say = ({ input }, node, check, message) => input.report({ check, loc: node.loc, message });
131
130
  const textOf = ({ input }, node) => input.context.sourceCode.getText(node);
132
131
  const describeIn = (at, node) => describe(node, (part) => textOf(at, part));
133
132
  function removeWhen(at) {
134
133
  const { node } = at.input;
135
134
  const when = getAttribute(node, 'when');
136
135
  if (!when) {
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
+ // tracker 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.`);
138
138
  return null;
139
139
  }
140
140
  const value = getExpression(when);
141
141
  const what = when.value ? `\`${at.input.context.sourceCode.getText(when.value)}\`` : 'empty';
142
+ // tracker CONV-parseJsx.ts:1113
142
143
  if (!value)
143
- say(at, when, 'invalid_remove_condition', `\`when\` on ${at.owner} is ${what}. Write the condition in braces: \`when={…}\`.`, 'CONV-parseJsx.ts:1113');
144
+ say(at, when, 'invalid_remove_condition', `\`when\` on ${at.owner} is ${what}. Write the condition in braces: \`when={…}\`.`);
144
145
  return value;
145
146
  }
146
147
  function removeTarget(at) {
@@ -153,30 +154,48 @@ function removeTarget(at) {
153
154
  const item = getObject(value.arguments[0]);
154
155
  // An item of another type is the bindings' own line (checkRawBindingsType).
155
156
  if (item && stringValue(getProperty(item, 'type')?.value) === BINDINGS_LIST && setsRemoved(item)) {
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');
157
+ // tracker CONV-decode.ts:280
158
+ 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.`);
157
159
  }
158
160
  }
159
161
  }
160
162
  function builderInPlace(at) {
161
163
  const { node, rulesApi } = at.input;
162
164
  for (const [call, where] of misplacedBuilders(node, rulesApi)) {
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');
165
+ // tracker CONV-astValue.ts:98
166
+ 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\`.`);
164
167
  }
165
168
  }
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). */
169
+ /**
170
+ * A callback prop takes a function: on a component that takes data, the import reads a `callback()` attribute as a binding (parseJsx.ts
171
+ * isCallbackAttr), and refuses an `on…` attribute with any other call. In `bindings.props` the site's runtime throws on anything but `callback()`
172
+ * (withOrigami).
173
+ */
167
174
  function checkCallbackProps(at) {
168
175
  const { node, rulesApi } = at.input;
169
- for (const [name, attribute] of getCallbackAttributes(node)) {
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');
176
+ const takesData = rulesApi.hasDataAttributes(node);
177
+ for (const [name, attribute, value] of getCallbackAttributes(node)) {
178
+ const isCallback = builderOf(value, rulesApi) === 'callback';
179
+ // tracker OUR-031
180
+ if (!takesData)
181
+ say(at, attribute, 'callback_attribute', `\`${name}\` on ${at.owner} is an attribute, which the import doesn't read on this component. ${CALLBACK_FIX(`bindings={{ props: { ${name}: callback(…) } }}`)}`);
182
+ else if (!isCallback)
183
+ say(at, attribute, 'callback_attribute', `\`${name}\` on ${at.owner} is ${describeCall(at, value)}, which the import doesn't read for a callback prop. ${CALLBACK_FIX(`${name}={callback(…)}`)}`);
171
184
  }
172
185
  if (!isWrappedByRuntime(node, rulesApi))
173
186
  return;
174
187
  for (const [name, value] of getBoundCallbacks(getAttribute(node, 'bindings'))) {
175
188
  const what = runsAtRender(at, value);
189
+ // tracker OUR-031
176
190
  if (what)
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');
191
+ say(at, value, 'non_function_callback', `\`bindings.props.${name}\` on ${at.owner} is ${what}, which gives a value, not a function: the site's runtime throws on it. ${CALLBACK_FIX(`${name}: callback(…)`)}`);
178
192
  }
179
193
  }
194
+ const CALLBACK_FIX = (form) => `Write \`${form}\`, its first argument an imported function, a \`ctx()\` or a member of the renderItem row.`;
195
+ const describeCall = (at, node) => {
196
+ const builder = builderOf(node, at.input.rulesApi);
197
+ return builder ? `\`${builder}()\`` : describeIn(at, node);
198
+ };
180
199
  function boundPropOnce(at) {
181
200
  const { node, rulesApi } = at.input;
182
201
  const bindings = getAttribute(node, 'bindings');
@@ -185,8 +204,9 @@ function boundPropOnce(at) {
185
204
  return;
186
205
  for (const [name, attribute] of boundAttributes(node, rulesApi)) {
187
206
  const where = props.raw ? `bindings={raw({ properties: { ${name} } })}` : `bindings={{ props: { ${name} } }}`;
207
+ // tracker CONV-parseJsx.ts:753
188
208
  if (props.names.has(name))
189
- say(at, attribute, 'duplicate_binding', `${at.owner} binds \`${name}\` twice: in \`${name}={…}\` and in \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
209
+ say(at, attribute, 'duplicate_binding', `${at.owner} binds \`${name}\` twice: in \`${name}={…}\` and in \`${where}\`. Keep one.`);
190
210
  }
191
211
  }
192
212
  function entryPropBoundOnce(at, entry) {
@@ -195,8 +215,9 @@ function entryPropBoundOnce(at, entry) {
195
215
  return;
196
216
  for (const [key, property] of boundEntryKeys(entry, at.input.rulesApi)) {
197
217
  const where = props.raw ? `bindings: raw({ properties: { ${key} } })` : `bindings.props.${key}`;
218
+ // tracker CONV-parseJsx.ts:753
198
219
  if (props.names.has(key))
199
- say(at, property, 'duplicate_binding', `${at.owner} binds \`${key}\` twice: in \`${key}: …\` and in its \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
220
+ say(at, property, 'duplicate_binding', `${at.owner} binds \`${key}\` twice: in \`${key}: …\` and in its \`${where}\`. Keep one.`);
200
221
  }
201
222
  }
202
223
  function bindingsAttribute(at, attribute) {
@@ -204,9 +225,9 @@ function bindingsAttribute(at, attribute) {
204
225
  return;
205
226
  const value = getExpression(attribute);
206
227
  if (value)
207
- bindingsValue(at, value);
208
- else
209
- say(at, attribute, 'invalid_bindings_object', notBindings(at, attribute.value ? `\`${at.input.context.sourceCode.getText(attribute.value)}\`` : 'empty'), 'CONV-parseJsx.ts:700');
228
+ return bindingsValue(at, value);
229
+ // tracker CONV-parseJsx.ts:700
230
+ say(at, attribute, 'invalid_bindings_object', notBindings(at, attribute.value ? `\`${at.input.context.sourceCode.getText(attribute.value)}\`` : 'empty'));
210
231
  }
211
232
  const notBindings = (at, what) => `\`bindings\` on ${at.owner} is ${what}. Write an object in place, \`bindings={{ props: {…} }}\`, or \`raw({…})\`.`;
212
233
  function bindingsValue(at, node, isEntry = false) {
@@ -221,22 +242,27 @@ function bindingsValue(at, node, isEntry = false) {
221
242
  const object = bare(item);
222
243
  if (!isEntry)
223
244
  checkRawBindingsType(at, value, object);
245
+ // tracker CONV-decode.ts:280
224
246
  if (!isEntry && setsRemoved(object))
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');
247
+ 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={…}>\`.`);
226
248
  const propertyMap = getProperty(object, 'properties');
227
249
  const properties = getObject(propertyMap?.value);
250
+ // tracker CONV-decode.ts:252
228
251
  if (propertyMap && !properties)
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');
252
+ 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', … } }\`.`);
230
253
  for (const property of properties ? getProperties(properties) : []) {
254
+ // tracker CONV-decode.ts:252
231
255
  if (isStaticItem(property.value))
232
- say(at, property.value, 'static_binding_value', fixedValue(at, getKey(property)), 'CONV-decode.ts:252');
256
+ say(at, property.value, 'static_binding_value', fixedValue(at, getKey(property)));
233
257
  }
234
258
  }
235
259
  else if (builder) {
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');
260
+ // tracker CONV-decode.ts:263
261
+ 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({…})\`.`);
237
262
  }
238
263
  else if (!isCallByName(value)) {
239
- say(at, value, 'invalid_bindings_object', notBindings(at, describeIn(at, value)), 'CONV-parseJsx.ts:700');
264
+ // tracker CONV-parseJsx.ts:700; an elementProps entry's: CONV-innerElements.ts:221
265
+ say(at, value, 'invalid_bindings_object', notBindings(at, describeIn(at, value)));
240
266
  }
241
267
  }
242
268
  function bindingsObject(at, bindings, isEntry) {
@@ -247,8 +273,9 @@ function bindingsObject(at, bindings, isEntry) {
247
273
  : property.key.type !== 'Identifier' ? `the key \`${textOf(at, property.key)}\``
248
274
  : property.key.name === BINDINGS_KEY || CLASS_NAME_SECTIONS.includes(property.key.name) ? null
249
275
  : `the key \`${property.key.name}\``;
276
+ // tracker CONV-parseJsx.ts:708
250
277
  if (what)
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');
278
+ say(at, property, 'invalid_bindings_key', `\`bindings\` on ${at.owner} has ${what}. Use only the key props, written as a plain name.`);
252
279
  else if (getKey(property) === BINDINGS_KEY)
253
280
  section(at, BINDINGS_KEY, property.value);
254
281
  else
@@ -259,17 +286,20 @@ function bindingsObject(at, bindings, isEntry) {
259
286
  function classNameSection(at, property, isEntry) {
260
287
  const key = getKey(property);
261
288
  const removed = key === 'visibility' && !isEntry && getObject(property.value) && getProperty(getObject(property.value), REMOVED);
289
+ // tracker CONV-decode.ts:280
262
290
  if (removed)
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');
291
+ 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={…}>\`.`);
264
292
  const term = key === 'states' ? '{ hover: is(…) }' : "{ 'wix-visibility-hidden': is(…) }";
265
293
  const fix = isEntry ? `in the entry's \`className: ${term}\`` : `as a \`className\` term: \`className={['comp-x', ${term}]}\``;
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');
294
+ // tracker CONV-parseJsx.ts:708
295
+ 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}.`);
267
296
  }
268
297
  function section(at, name, node) {
269
298
  const fix = `Write an object of \`key: value\` entries: \`${name}: { label: … }\`.`;
270
299
  const value = bare(node);
300
+ // tracker CONV-parseJsx.ts:669
271
301
  if (value.type !== 'ObjectExpression')
272
- return say(at, value, 'invalid_binding_map', `\`bindings.${name}\` on ${at.owner} isn't an object. ${fix}`, 'CONV-parseJsx.ts:669');
302
+ return say(at, value, 'invalid_binding_map', `\`bindings.${name}\` on ${at.owner} isn't an object. ${fix}`);
273
303
  for (const property of value.properties) {
274
304
  const problem = property.type === 'SpreadElement' ? 'has a spread'
275
305
  : property.computed ? 'has a computed key'
@@ -279,7 +309,8 @@ function section(at, name, node) {
279
309
  : getKey(property) === null ? `has the key \`${textOf(at, property.key)}\``
280
310
  : null;
281
311
  if (problem) {
282
- say(at, property, 'invalid_binding_map_entry', `\`bindings.${name}\` on ${at.owner} ${problem}. ${fix}`, 'CONV-parseJsx.ts:669');
312
+ // tracker CONV-parseJsx.ts:669
313
+ say(at, property, 'invalid_binding_map_entry', `\`bindings.${name}\` on ${at.owner} ${problem}. ${fix}`);
283
314
  continue;
284
315
  }
285
316
  prop(at, getKey(property), property.value, false);
@@ -301,7 +332,8 @@ function classNameConditions(at, terms, where) {
301
332
  if (key === null)
302
333
  continue;
303
334
  if (key.startsWith(VISIBILITY_PREFIX) && !VISIBILITY_KEYS.includes(key.slice(VISIBILITY_PREFIX.length))) {
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');
335
+ // tracker CONV-decode.ts:221
336
+ 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\`.`);
305
337
  }
306
338
  else
307
339
  condition(at, property.value, `${where} term \`${key}\``);
@@ -309,10 +341,12 @@ function classNameConditions(at, terms, where) {
309
341
  }
310
342
  function prop(at, name, node, placeholderAllowed) {
311
343
  const value = bare(node);
344
+ // tracker CONV-decode.ts:209
312
345
  if (value.type === 'ArrayExpression')
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');
346
+ 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()\`.`);
347
+ // tracker CONV-decode.ts:252
314
348
  if (isPlain(value) || rawOnly(value) || isStaticRaw(at, value) || isTyped(value, at.input.rulesApi))
315
- return say(at, value, 'static_binding_value', fixedValue(at, name), 'CONV-decode.ts:252');
349
+ return say(at, value, 'static_binding_value', fixedValue(at, name));
316
350
  binding(at, value, 'prop', placeholderAllowed);
317
351
  }
318
352
  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.`;
@@ -328,7 +362,8 @@ function condition(at, node, where, inRemove = false) {
328
362
  return binding(at, value, 'condition');
329
363
  if (inRemove || value.elements.length > 0) {
330
364
  const spread = hasHoleOrSpread(value);
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');
365
+ // tracker CONV-decode.ts:190; a list with a spread or a hole: CONV-parseJsx.ts:655
366
+ 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, …)\`.`);
332
367
  }
333
368
  }
334
369
  function binding(at, node, place, placeholderAllowed = false) {
@@ -357,30 +392,35 @@ function binding(at, node, place, placeholderAllowed = false) {
357
392
  const raw = rawOnly(value);
358
393
  if (raw === 'an object' && place === 'fnArg' && isDataItem(value))
359
394
  return;
395
+ // tracker CONV-decode.ts:86
360
396
  if (raw === 'an object' && place === 'fnArg')
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');
397
+ 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()\`.`);
362
398
  if (raw)
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');
399
+ return say(at, value, 'invalid_binding_value', `${capitalize(name)} on ${at.owner} is ${raw}. Write ${PLAIN}, or wrap it in \`raw()\`.`);
400
+ say(at, value, 'invalid_binding_value', `${capitalize(name)} on ${at.owner} is ${describeIn(at, value)}. Write it out. ${hintFor(place)}`);
365
401
  }
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');
402
+ // tracker CONV-decode.ts:126
403
+ 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)}`);
367
404
  /** A called ctx() or row member: `ctx('cart.add', { from: useCart })(item.id)`, `item.actions.select('quick')` (decode.ts decodeInvoke). */
368
405
  function invoke(at, node) {
369
406
  const { callee } = node;
407
+ // tracker CONV-decode.ts:86
370
408
  if (!isMethodTarget(callee, at.input.rulesApi))
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');
409
+ 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)\`.`);
372
410
  if (callee.type === 'CallExpression')
373
411
  ctxCall(at, callee, false);
412
+ // tracker CONV-astValue.ts:310
374
413
  if (node.arguments.some((argument) => argument.type === 'SpreadElement'))
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');
414
+ return say(at, node, 'spread_builder_arguments', `A called \`ctx()\` or row member on ${at.owner} spreads its arguments. Write each argument out.`);
376
415
  for (const argument of node.arguments)
377
416
  binding(at, argument, 'fnArg');
378
417
  }
379
418
  function call(at, node, builder, place, placeholderAllowed) {
380
419
  if (node.arguments.some((argument) => argument.type === 'SpreadElement')) {
381
- return say(at, node, 'spread_builder_arguments', `\`${builder}()\` on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
420
+ // tracker CONV-astValue.ts:310
421
+ return say(at, node, 'spread_builder_arguments', `\`${builder}()\` on ${at.owner} spreads its arguments. Write each argument out.`);
382
422
  }
383
- // map() and shape() hold a callback, read in its own scope: no tracker row checks its shape yet.
423
+ // map() and shape() hold a callback, read in its own scope: nothing checks its shape yet.
384
424
  if (builder === 'map' || builder === 'shape' || hasDuplicateKey(at, node, builder))
385
425
  return;
386
426
  if (builder === 'ctx')
@@ -398,8 +438,9 @@ function call(at, node, builder, place, placeholderAllowed) {
398
438
  }
399
439
  function hasDuplicateKey(at, node, builder) {
400
440
  const twice = node.arguments.map(duplicateKeyIn).find(Boolean);
441
+ // tracker, by builder: all CONV-decode.ts:196, callback and fn CONV-astValue.ts:287, ctx CONV-astValue.ts:276, is CONV-decode.ts:172, raw CONV-decode.ts:28
401
442
  if (twice)
402
- say(at, twice, 'duplicate_builder_key', `\`${builder}()\` on ${at.owner} has the key \`${getKey(twice)}\` twice. Keep one.`, ROW_OF_BUILDER[builder]);
443
+ say(at, twice, 'duplicate_builder_key', `\`${builder}()\` on ${at.owner} has the key \`${getKey(twice)}\` twice. Keep one.`);
403
444
  return Boolean(twice);
404
445
  }
405
446
  function ctxCall(at, node, placeholderAllowed) {
@@ -415,16 +456,18 @@ function ctxCall(at, node, placeholderAllowed) {
415
456
  : !keys.includes('from') ? 'has no `from`'
416
457
  : keys.includes('placeholder') && !placeholderAllowed ? 'has a placeholder: only a whole attribute value, or an elementProps entry value, has one'
417
458
  : null;
459
+ // tracker CONV-astValue.ts:276
418
460
  if (problem)
419
- return say(at, node, 'invalid_context_arguments', `\`ctx()\` on ${at.owner} ${problem}. Write \`ctx('item.path', { from: useX })\`.`, 'CONV-astValue.ts:276');
461
+ return say(at, node, 'invalid_context_arguments', `\`ctx()\` on ${at.owner} ${problem}. Write \`ctx('item.path', { from: useX })\`.`);
420
462
  const placeholder = object && getProperty(object, 'placeholder');
421
463
  if (placeholder)
422
464
  placeholderValue(at, placeholder.value);
423
465
  }
424
466
  function fnCall(at, node, placeholderAllowed) {
425
467
  const [, ...rest] = node.arguments;
468
+ // tracker CONV-astValue.ts:287
426
469
  if (node.arguments.length === 0)
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');
470
+ return say(at, node, 'missing_function_argument', `\`fn()\` on ${at.owner} has no arguments. Write the imported function first: \`fn(numberToText, …)\`.`);
428
471
  const last = rest.at(-1);
429
472
  const options = placeholderAllowed && last ? getObject(last) : null;
430
473
  const placeholder = options && options.properties.length === 1 ? getProperty(options, 'placeholder') : undefined;
@@ -438,35 +481,44 @@ function fnCall(at, node, placeholderAllowed) {
438
481
  /** `callback(goHome, 2)`: the function (an imported one, a `ctx()` or a member of the row), then its bound arguments (decode.ts decodeCallback). */
439
482
  function callbackCall(at, node) {
440
483
  const [first, ...bound] = node.arguments;
484
+ // tracker CONV-astValue.ts:287
441
485
  if (!first)
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');
486
+ return say(at, node, 'missing_function_argument', `\`callback()\` on ${at.owner} has no arguments. Write the function first: \`callback(<function>, …)\`.`);
443
487
  const target = bare(first);
444
488
  // A name that isn't a parameter is the imported function: bindingNameImported checks its import.
445
489
  const isFunctionName = target.type === 'Identifier' && !getParamReadKind(target);
490
+ // tracker CONV-decode.ts:86
446
491
  if (!isFunctionName && !isMethodTarget(target, at.input.rulesApi))
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');
492
+ 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(<function>, …)\`.`);
448
493
  if (target.type === 'CallExpression')
449
494
  ctxCall(at, target, false);
450
495
  for (const argument of bound)
451
496
  binding(at, argument, 'fnArg');
452
497
  }
498
+ /** The import stores any JSON value but null as the prop's data (boundProps.ts isPlaceholderValue): the entries of a list, a rich text item. */
453
499
  function placeholderValue(at, node) {
454
500
  const value = bare(node);
455
- if (!isPlain(value))
456
- say(at, value, 'invalid_placeholder', `The placeholder on ${at.owner} is ${rawOnly(value) ?? describeIn(at, value)}. Write ${PLAIN}.`, 'CONV-parseJsx.ts:729');
501
+ const isNull = rawOnly(value) === 'null';
502
+ const found = isNull ? value : nonLiteralIn(value);
503
+ // tracker CONV-parseJsx.ts:729
504
+ if (found)
505
+ say(at, found, 'invalid_placeholder', `The placeholder on ${at.owner} ${found === value ? 'is' : 'holds'} ${isNull ? 'null' : describeIn(at, found)}. Write ${PLAIN}, or an object or array of them.`);
457
506
  }
458
507
  function isCall(at, node) {
459
508
  const fix = 'Write `is(<ctx() or fn()>, <string or true / false>)`.';
509
+ // tracker CONV-decode.ts:172
460
510
  if (node.arguments.length !== 2)
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');
511
+ return say(at, node, 'invalid_comparison_arguments', `\`is()\` on ${at.owner} has ${node.arguments.length} argument${node.arguments.length === 1 ? '' : 's'}. ${fix}`);
462
512
  const [binded, compared] = node.arguments.map((argument) => bare(argument));
513
+ // tracker CONV-decode.ts:172
463
514
  if (isPlain(binded) || rawOnly(binded) || builderOf(binded, at.input.rulesApi) === 'raw' || isTyped(binded, at.input.rulesApi))
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');
515
+ say(at, binded, 'literal_comparison_binding', `\`is()\` on ${at.owner} compares ${isCallByName(binded) ? `\`${builderOf(binded, at.input.rulesApi)}()\`` : 'a literal'}, not a binding. ${fix}`);
465
516
  else
466
517
  binding(at, binded, 'isBinding');
467
518
  const isStringOrBoolean = compared.type === 'Literal' && ['string', 'boolean'].includes(typeof compared.value);
519
+ // tracker CONV-decode.ts:172
468
520
  if (!isStringOrBoolean)
469
- say(at, compared, 'invalid_comparison_value', `\`is()\` on ${at.owner} compares to ${rawOnly(compared) ?? describeIn(at, compared)}. ${fix}`, 'CONV-decode.ts:172');
521
+ say(at, compared, 'invalid_comparison_value', `\`is()\` on ${at.owner} compares to ${rawOnly(compared) ?? describeIn(at, compared)}. ${fix}`);
470
522
  }
471
523
  /**
472
524
  * A component's `raw()` bindings are one item of the bindings namespace, and its one schema is BindingsList (reservedAttributes.ts
@@ -478,33 +530,39 @@ function checkRawBindingsType(at, node, item) {
478
530
  if (stringValue(type?.value) === BINDINGS_LIST)
479
531
  return;
480
532
  const has = type ? `has the type \`${textOf(at, type.value)}\`` : 'has no `type`';
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');
533
+ // tracker CONV-decode.ts:28; under a <Remove>, also CONV-decode.ts:296
534
+ 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.`);
482
535
  }
483
536
  function allCall(at, node, place) {
484
537
  const count = node.arguments.length;
538
+ // tracker CONV-decode.ts:196
485
539
  if (place === 'condition' && count < 2)
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');
540
+ 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()\`.`);
487
541
  for (const argument of node.arguments)
488
542
  binding(at, argument, 'allItem');
489
543
  }
490
544
  function isRawWrittenOut(at, node) {
491
545
  const [item, ...extra] = node.arguments;
492
546
  if (!item || extra.length > 0) {
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');
547
+ // tracker CONV-decode.ts:28
548
+ say(at, node, 'invalid_raw_arguments', `\`raw()\` on ${at.owner} has ${node.arguments.length} arguments. Write one item: \`raw({ type: …, … })\`.`);
494
549
  return false;
495
550
  }
496
551
  const value = bare(item);
497
552
  if (value.type === 'CallExpression') {
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');
553
+ // tracker CONV-decode.ts:28
554
+ 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()\`.`);
499
555
  return false;
500
556
  }
501
557
  if (value.type !== 'ObjectExpression') {
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');
558
+ // tracker CONV-decode.ts:28
559
+ say(at, value, 'non_object_raw', `\`raw()\` on ${at.owner} wraps ${rawOnly(value) ?? describeIn(at, value)}, not an object. Write one item: \`raw({ type: …, … })\`.`);
503
560
  return false;
504
561
  }
505
562
  const found = nonLiteralIn(value);
563
+ // tracker CONV-decode.ts:28
506
564
  if (found)
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');
565
+ say(at, found, 'non_literal_raw', `\`raw()\` on ${at.owner} holds ${describeIn(at, found)}. Write its item out with literal values.`);
508
566
  return !found;
509
567
  }
510
568
  /** Only BUILDERS: other runtime names (`Remove(…)`) pass the converter's parse, then its decode has no case for them. */
@@ -513,11 +571,12 @@ function isBuilderImported({ rulesApi, report }, callee) {
513
571
  if (found?.kind === 'runtime' && found.imported === callee.name && BUILDERS.includes(callee.name))
514
572
  return true;
515
573
  const builder = found?.kind === 'runtime' ? found.imported : callee.name;
574
+ // tracker CONV-astValue.ts:296
516
575
  report({ check: 'invalid_builder_import',
517
576
  loc: callee.loc,
518
- message: tracked(BUILDERS.includes(builder)
577
+ message: BUILDERS.includes(builder)
519
578
  ? `\`${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}(…)\`.`
520
- : `\`${callee.name}(…)\` isn't a builder. In an attribute, only the builders of ${RUNTIME_PACKAGE} are called (${BUILDERS.join(', ')}); write a literal value, or one of them.`, 'CONV-astValue.ts:296'),
579
+ : `\`${callee.name}(…)\` isn't a builder. In an attribute, only the builders of ${RUNTIME_PACKAGE} are called (${BUILDERS.join(', ')}); write a literal value, or one of them.`,
521
580
  });
522
581
  return false;
523
582
  }
@@ -532,25 +591,28 @@ function bindingNameImported({ node, rulesApi, report, context }, builder) {
532
591
  const value = bare(named);
533
592
  const what = builder === 'fn' ? "fn()'s function" : builder === 'callback' ? "callback()'s function" : "ctx()'s `from` hook";
534
593
  if (value.type !== 'Identifier') {
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') });
594
+ // tracker CONV-astValue.ts:227
595
+ report({ check: 'non_identifier_binding_name', loc: value.loc, message: `${what} is \`${context.sourceCode.getText(value)}\`. Write an imported name: \`fn(numberToText, …)\`, \`{ from: useProduct }\`.` });
536
596
  return null;
537
597
  }
538
598
  const found = rulesApi.getImport(value);
539
599
  const problem = !found
540
- ? [`${what} \`${value.name}\` isn't imported. Import it by name from its library: the function's library, or the context's app.`, 'CONV-astValue.ts:227']
600
+ ? `${what} \`${value.name}\` isn't imported. Import it by name from its library: the function's library, or the context's app.`
541
601
  : found.kind && RESERVED_FOR_BINDINGS.includes(found.kind)
542
- ? [`${what} \`${value.name}\` comes from ${found.source}, which holds no functions or contexts. Import it from its own library: the function's library, or the context's app.`, 'CONV-astValue.ts:230']
602
+ ? `${what} \`${value.name}\` comes from ${found.source}, which holds no functions or contexts. Import it from its own library: the function's library, or the context's app.`
543
603
  : found.imported === '*'
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']
604
+ ? `${what} \`${value.name}\` is a namespace (\`import * as ${value.name}\`). Import the ${builder === 'fn' ? 'function' : 'hook'} itself by name: \`import { name } from '${found.source}'\`.`
545
605
  : null;
606
+ // tracker CONV-astValue.ts:227; from a library that holds no functions or contexts: CONV-astValue.ts:230
546
607
  if (problem)
547
- report({ check: 'invalid_binding_import', loc: value.loc, message: tracked(...problem) });
608
+ report({ check: 'invalid_binding_import', loc: value.loc, message: problem });
548
609
  return found && !problem && builder === 'ctx' ? { hook: value, source: found.source } : null;
549
610
  }
550
611
  function ctxSourceHasName({ report }, { hook, source }) {
551
612
  if (source.indexOf('/') !== source.length - 1)
552
613
  return;
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') });
614
+ // tracker CONV-decode.ts:100
615
+ report({ check: 'missing_context_name', loc: hook.loc, message: `ctx()'s \`from\` hook \`${hook.name}\` is imported from '${source}', which has no context name after the \`/\`. Write '<componentType>/<contextName>'.` });
554
616
  }
555
617
  // ── reading the code ──
556
618
  function getElementPropsEntries(element) {
@@ -656,9 +718,12 @@ function isWrappedByRuntime(element, rulesApi) {
656
718
  const kind = rulesApi.getImport(element)?.kind;
657
719
  return kind === 'wrapper' || kind === 'siteCode';
658
720
  }
659
- /** A callback prop written as an attribute with a call: `onClick={fn(goHome)}`. */
721
+ /** A callback prop written as an attribute with a call: `onClick={callback(goHome)}`, `onClick={fn(goHome)}`. */
660
722
  function getCallbackAttributes(element) {
661
- return element.openingElement.attributes.flatMap((attribute) => attribute.type === 'JSXAttribute' && CALLBACK_PROP.test(getName(attribute)) && getExpression(attribute)?.type === 'CallExpression' ? [[getName(attribute), attribute]] : []);
723
+ return element.openingElement.attributes.flatMap((attribute) => {
724
+ const value = attribute.type === 'JSXAttribute' && CALLBACK_PROP.test(getName(attribute)) ? getExpression(attribute) : null;
725
+ return value?.type === 'CallExpression' ? [[getName(attribute), attribute, value]] : [];
726
+ });
662
727
  }
663
728
  /** The callback props in `bindings={{ props: { onClick: … } }}`. */
664
729
  function getBoundCallbacks(attribute) {
@@ -676,12 +741,14 @@ function runsAtRender(at, node) {
676
741
  return RENDER_BUILDERS.includes(builder) ? `\`${builder}()\`` : null;
677
742
  return isInvoke(node) || getParamReadKind(node) ? describeIn(at, node) : null;
678
743
  }
679
- /** The data attributes bound to a value the import reads as a binding; not an `on…` attribute, which it doesn't read at all (OUR-031 reports it). */
744
+ /** The data attributes bound to a value the import reads as a binding; an `on…` attribute only with `callback()` (checkCallbackProps reports the rest). */
680
745
  function boundAttributes(element, rulesApi) {
681
746
  return element.openingElement.attributes.flatMap((attribute) => {
682
- if (attribute.type !== 'JSXAttribute' || isReservedAttribute(getName(attribute)) || EVENT_ATTRIBUTE.test(getName(attribute)))
747
+ if (attribute.type !== 'JSXAttribute' || isReservedAttribute(getName(attribute)))
683
748
  return [];
684
749
  const value = getExpression(attribute);
750
+ if (EVENT_ATTRIBUTE.test(getName(attribute)) && !(value && builderOf(value, rulesApi) === 'callback'))
751
+ return [];
685
752
  return isBound(value) && !(value.type === 'CallExpression' && isSvgCall(value, rulesApi)) ? [[getName(attribute), attribute, value]] : [];
686
753
  });
687
754
  }
@@ -36,7 +36,7 @@ function isWellFormed(file, declaration, report) {
36
36
  if (hasShape(declaration))
37
37
  return true;
38
38
  const params = `${declaration.name} ${declaration.query}`.trim();
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).`);
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);\`.`);
40
40
  return false;
41
41
  }
42
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). */
@@ -44,7 +44,7 @@ function isFirstOfName(file, declaration, first, report) {
44
44
  const earlier = first.get(declaration.name);
45
45
  if (!earlier)
46
46
  return first.set(declaration.name, declaration), true;
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).`);
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.`);
48
48
  return false;
49
49
  }
50
50
  /** Reports a page file's declaration whose name isn't one of the site's (CONV-astParser.ts:335). */
@@ -52,7 +52,7 @@ function isSiteName(file, declaration, site, report) {
52
52
  const names = site.breakpoints.map(({ name }) => name);
53
53
  if (names.includes(declaration.name))
54
54
  return true;
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).`);
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.`);
56
56
  return false;
57
57
  }
58
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). */
@@ -60,7 +60,7 @@ function keepsSiteRange(file, declaration, site, report) {
60
60
  const exported = getSiteRange(declaration.name, site);
61
61
  if (exported.trim() === declaration.query.trim())
62
62
  return true;
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).`);
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.`);
64
64
  return false;
65
65
  }
66
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. */
@@ -73,7 +73,7 @@ function checkNamedUses(file, site, report) {
73
73
  const fix = range
74
74
  ? `add \`@custom-media ${name} ${range};\` at the top of ${file.name}, or write the range itself, \`@media ${range}\``
75
75
  : `add \`@custom-media ${name} (<its range>);\` at the top of ${file.name} (the range the site gives it), or write the range itself`;
76
- reportCssProblem(report, file.name, line, 'undeclared_breakpoint', `\`@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}.`);
77
77
  }
78
78
  }
79
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. */
@@ -86,7 +86,7 @@ function checkRanges(file, site, report) {
86
86
  const query = resolve(media, file.declared, site);
87
87
  if (query === null || site.breakpoints.some((breakpoint) => canonical(breakpoint.query) === canonical(query)))
88
88
  continue;
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.`);
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)}. The import refuses a value for a breakpoint the site doesn't have.`);
90
90
  }
91
91
  }
92
92
  // ── reading the files ──