@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.
- package/dist/bi/read-stats.js +9 -2
- package/dist/bi/stats.js +3 -2
- package/dist/project-rules-runner/run-project-rules.js +10 -4
- package/dist/rules/index.js +1 -0
- package/dist/rules/project/home-page-folder.js +6 -4
- package/dist/rules/project/index.js +1 -0
- package/dist/rules/site-app/allowed-imports.js +9 -7
- package/dist/rules/site-app/apply-and-presets.js +15 -13
- package/dist/rules/site-app/bindings.js +53 -51
- package/dist/rules/site-app/breakpoints.js +8 -6
- package/dist/rules/site-app/css-file-nesting.js +15 -13
- package/dist/rules/site-app/css-layout-properties.js +16 -14
- package/dist/rules/site-app/css-selectors.js +20 -18
- package/dist/rules/site-app/data-values.js +20 -18
- package/dist/rules/site-app/global-styles.js +18 -16
- package/dist/rules/site-app/no-absolute-section.js +3 -1
- package/dist/rules/site-app/no-free-code.js +6 -4
- package/dist/rules/site-app/page-structure.js +16 -14
- package/dist/rules/site-app/slot-attributes.js +6 -4
- package/dist/rules/site-app/theme-font-ids.js +3 -1
- package/dist/rules/site-app/theme-root.js +9 -7
- package/dist/rules-adapter/oxlint/adapt-rule.js +1 -1
- package/dist/rules-adapter/utils.js +5 -2
- package/dist/rules-api/report.js +3 -3
- package/package.json +3 -3
|
@@ -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
|
}
|