@wix/origami-lint 1.34.4 → 1.34.6
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/rules/bindings.js +210 -103
- package/dist/rules/css-selectors.js +270 -0
- package/dist/rules/data-values.js +17 -26
- package/dist/rules/index.js +2 -1
- package/dist/rules-api/index.js +3 -2
- package/dist/rules-api/rules-api.js +3 -1
- package/dist/shared/constants.js +41 -0
- package/dist/shared/css.js +23 -23
- package/dist/shared/utils.js +74 -39
- package/package.json +2 -2
package/dist/rules/bindings.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import {
|
|
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
3
|
/** The converter's reserved sources (identity.ts `isReservedRegistry`) and the DM's own `document`: no fn()/ctx() names. */
|
|
4
4
|
const RESERVED_FOR_BINDINGS = ['runtime', 'interactReact', 'interact', 'motionPresets', 'menu', 'document', 'core'];
|
|
5
|
-
/** The
|
|
6
|
-
const
|
|
5
|
+
/** The key of a `bindings` object (parseJsx.ts bindingsIrOfExpression); the other two sections are written in `className` since converter 0.79. */
|
|
6
|
+
const BINDINGS_KEY = 'props';
|
|
7
|
+
const CLASS_NAME_SECTIONS = ['visibility', 'states'];
|
|
8
|
+
/** A `className` term for a visibility: the viewer's utility class (reservedAttributes.ts CLASS_NAME_UTILITIES). */
|
|
9
|
+
const VISIBILITY_PREFIX = 'wix-visibility-';
|
|
7
10
|
const VISIBILITY_KEYS = ['collapsed', 'hidden'];
|
|
8
11
|
const REMOVED = 'removed';
|
|
9
12
|
const CTX_OPTIONS = ['from', 'placeholder'];
|
|
10
|
-
const STATE_OPTIONS = ['kind', 'id', 'pseudoClass'];
|
|
11
13
|
const BINDINGS_LIST = 'BindingsList';
|
|
12
14
|
const STATIC_PREFIX = 'Static.';
|
|
15
|
+
/** The types of an object the import reads as an argument's data item (decode.ts decodeDataRef); an object with no type and an `href` is a link. */
|
|
16
|
+
const DATA_ITEM_TYPES = ['Builder.Image', 'Builder.Video', 'Builder.VectorArt', 'Builder.Audio', 'Builder.RichText', 'Builder.MenuItems', 'Builder.MenuItem', 'PageLink', 'ExternalLink',
|
|
17
|
+
'EmailLink', 'AnchorLink', 'DocumentLink', 'PhoneLink', 'DynamicPageLink', 'TextLink', 'LoginToWixLink', 'WhatsAppLink', 'AddressLink', 'TpaPageLink'];
|
|
13
18
|
const PLACES = {
|
|
14
|
-
prop: { name: "a prop's binding", builders: ['ctx', 'fn', 'raw'], literals: false },
|
|
15
|
-
fnArg: { name: 'an `fn()` argument', builders: ['ctx', 'fn', 'raw'], literals: true },
|
|
16
|
-
isBinding: { name: "`is()`'s binding", builders: ['ctx', 'fn'], literals: false },
|
|
17
|
-
condition: { name: 'a condition', builders: ['is', 'all', 'ctx', 'fn', 'raw'], literals: true },
|
|
18
|
-
allItem: { name: 'an `all()` item', builders: ['is', 'ctx', 'fn', 'raw'], literals: true },
|
|
19
|
-
statesItem: { name: 'a `states` item', builders: ['state', 'raw'], literals: false },
|
|
19
|
+
prop: { name: "a prop's binding", builders: ['ctx', 'fn', 'raw', 'callback', 'map', 'shape'], literals: false },
|
|
20
|
+
fnArg: { name: 'an `fn()` argument', builders: ['ctx', 'fn', 'raw', 'callback', 'map', 'shape'], literals: true },
|
|
21
|
+
isBinding: { name: "`is()`'s binding", builders: ['ctx', 'fn', 'callback'], literals: false },
|
|
22
|
+
condition: { name: 'a condition', builders: ['is', 'all', 'ctx', 'fn', 'raw', 'callback'], literals: true },
|
|
23
|
+
allItem: { name: 'an `all()` item', builders: ['is', 'ctx', 'fn', 'raw', 'callback'], literals: true },
|
|
20
24
|
};
|
|
21
25
|
const PLAIN = 'a string, number or true / false';
|
|
26
|
+
/** The builders whose value runs at render: a callback prop gets their value, not a function. */
|
|
27
|
+
const RENDER_BUILDERS = ['ctx', 'fn', ...TRANSFORM_BUILDERS];
|
|
22
28
|
export default defineRule({
|
|
23
29
|
name: 'bindings',
|
|
24
30
|
editors: ['studio2'],
|
|
@@ -47,13 +53,14 @@ function checkRemoveCondition(at) {
|
|
|
47
53
|
condition(at, when, '`when`', true);
|
|
48
54
|
removeTarget(at);
|
|
49
55
|
}
|
|
50
|
-
/** A component's builders, its `
|
|
56
|
+
/** A component's builders, its callback props, its `bindings` and `className` conditions, and the props it binds as data attributes. */
|
|
51
57
|
function checkComponentBindings(at) {
|
|
52
|
-
const { node } = at.input;
|
|
58
|
+
const { node, rulesApi } = at.input;
|
|
53
59
|
builderInPlace(at);
|
|
54
|
-
|
|
60
|
+
checkCallbackProps(at);
|
|
55
61
|
bindingsAttribute(at, getAttribute(node, 'bindings'));
|
|
56
|
-
|
|
62
|
+
classNameAttribute(at, getAttribute(node, 'className'));
|
|
63
|
+
if (!rulesApi.hasDataAttributes(node))
|
|
57
64
|
return;
|
|
58
65
|
boundPropOnce(at);
|
|
59
66
|
for (const [name, , value] of boundAttributes(node))
|
|
@@ -69,6 +76,9 @@ function checkEntryBindings(at, entry) {
|
|
|
69
76
|
const bindings = getProperty(object, 'bindings');
|
|
70
77
|
if (bindings)
|
|
71
78
|
bindingsValue(entryAt, bindings.value, true);
|
|
79
|
+
const className = getObject(getProperty(object, 'className')?.value);
|
|
80
|
+
if (className)
|
|
81
|
+
classNameConditions(entryAt, className, '`className`');
|
|
72
82
|
entryPropBoundOnce(entryAt, object);
|
|
73
83
|
for (const [key, property] of boundEntryKeys(object))
|
|
74
84
|
prop(entryAt, key, property.value, true);
|
|
@@ -84,7 +94,7 @@ function checkCallExpressionImports(input) {
|
|
|
84
94
|
ctxSourceHasName(input, ctxHook);
|
|
85
95
|
}
|
|
86
96
|
/** Each builder's own row: the shape of its call, a key written twice in it. */
|
|
87
|
-
const ROW_OF_BUILDER = { all: 'CONV-decode.ts:196',
|
|
97
|
+
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' };
|
|
88
98
|
const tracked = (message, row) => `${message.slice(0, -1)} (tracker ${row}).`;
|
|
89
99
|
const say = ({ input }, node, message, row) => input.report({ loc: node.loc, message: tracked(message, row) });
|
|
90
100
|
const textOf = ({ input }, node) => input.context.sourceCode.getText(node);
|
|
@@ -110,12 +120,9 @@ function removeTarget(at) {
|
|
|
110
120
|
if (!value || builderOf(value, rulesApi) !== 'raw')
|
|
111
121
|
continue;
|
|
112
122
|
const item = getObject(value.arguments[0]);
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
say(at, value, `${
|
|
116
|
-
}
|
|
117
|
-
else if (setsRemoved(item)) {
|
|
118
|
-
say(at, value, `${target} of an item that already sets \`features.visibility.${REMOVED}\`. Keep only one of them.`, 'CONV-decode.ts:280');
|
|
123
|
+
// An item of another type is the bindings' own line (checkRawBindingsType).
|
|
124
|
+
if (item && stringValue(getProperty(item, 'type')?.value) === BINDINGS_LIST && setsRemoved(item)) {
|
|
125
|
+
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');
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
}
|
|
@@ -125,6 +132,20 @@ function builderInPlace(at) {
|
|
|
125
132
|
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');
|
|
126
133
|
}
|
|
127
134
|
}
|
|
135
|
+
/** 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). */
|
|
136
|
+
function checkCallbackProps(at) {
|
|
137
|
+
const { node, rulesApi } = at.input;
|
|
138
|
+
for (const [name, attribute] of getCallbackAttributes(node)) {
|
|
139
|
+
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');
|
|
140
|
+
}
|
|
141
|
+
if (!isWrappedByRuntime(node, rulesApi))
|
|
142
|
+
return;
|
|
143
|
+
for (const [name, value] of getBoundCallbacks(getAttribute(node, 'bindings'))) {
|
|
144
|
+
const what = runsAtRender(at, value);
|
|
145
|
+
if (what)
|
|
146
|
+
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');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
128
149
|
function boundPropOnce(at) {
|
|
129
150
|
const { node, rulesApi } = at.input;
|
|
130
151
|
const bindings = getAttribute(node, 'bindings');
|
|
@@ -156,7 +177,7 @@ function bindingsAttribute(at, attribute) {
|
|
|
156
177
|
else
|
|
157
178
|
say(at, attribute, notBindings(at, attribute.value ? `\`${at.input.context.sourceCode.getText(attribute.value)}\`` : 'empty'), 'CONV-parseJsx.ts:700');
|
|
158
179
|
}
|
|
159
|
-
const notBindings = (at, what) => `\`bindings\` on ${at.owner} is ${what}. Write an object in place, \`bindings={{ props: {…}
|
|
180
|
+
const notBindings = (at, what) => `\`bindings\` on ${at.owner} is ${what}. Write an object in place, \`bindings={{ props: {…} }}\`, or \`raw({…})\`.`;
|
|
160
181
|
function bindingsValue(at, node, isEntry = false) {
|
|
161
182
|
const value = bare(node);
|
|
162
183
|
if (value.type === 'ObjectExpression')
|
|
@@ -167,6 +188,8 @@ function bindingsValue(at, node, isEntry = false) {
|
|
|
167
188
|
if (hasDuplicateKey(at, value, 'raw') || !isRawWrittenOut(at, value))
|
|
168
189
|
return;
|
|
169
190
|
const object = bare(item);
|
|
191
|
+
if (!isEntry)
|
|
192
|
+
checkRawBindingsType(at, value, object);
|
|
170
193
|
if (!isEntry && setsRemoved(object))
|
|
171
194
|
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');
|
|
172
195
|
const propertyMap = getProperty(object, 'properties');
|
|
@@ -179,7 +202,7 @@ function bindingsValue(at, node, isEntry = false) {
|
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
204
|
else if (builder) {
|
|
182
|
-
say(at, value, `\`bindings\` on ${at.owner} is a \`${builder}()\` call. Write an object and put the call under props,
|
|
205
|
+
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');
|
|
183
206
|
}
|
|
184
207
|
else if (!isCallByName(value)) {
|
|
185
208
|
say(at, value, notBindings(at, describeIn(at, value)), 'CONV-parseJsx.ts:700');
|
|
@@ -191,15 +214,27 @@ function bindingsObject(at, bindings, isEntry) {
|
|
|
191
214
|
: property.computed ? 'a computed key'
|
|
192
215
|
: property.key.type === 'Literal' && typeof property.key.value === 'string' ? `the quoted key \`${textOf(at, property.key)}\``
|
|
193
216
|
: property.key.type !== 'Identifier' ? `the key \`${textOf(at, property.key)}\``
|
|
194
|
-
:
|
|
217
|
+
: property.key.name === BINDINGS_KEY || CLASS_NAME_SECTIONS.includes(property.key.name) ? null
|
|
195
218
|
: `the key \`${property.key.name}\``;
|
|
196
219
|
if (what)
|
|
197
|
-
say(at, property, `\`bindings\` on ${at.owner} has ${what}. Use only the
|
|
220
|
+
say(at, property, `\`bindings\` on ${at.owner} has ${what}. Use only the key props, written as a plain name.`, 'CONV-parseJsx.ts:708');
|
|
221
|
+
else if (getKey(property) === BINDINGS_KEY)
|
|
222
|
+
section(at, BINDINGS_KEY, property.value);
|
|
198
223
|
else
|
|
199
|
-
|
|
224
|
+
classNameSection(at, property, isEntry);
|
|
200
225
|
}
|
|
201
226
|
}
|
|
202
|
-
|
|
227
|
+
/** `bindings.visibility` and `bindings.states`: the import reads them in `className` since converter 0.79, and `removed` in `<Remove when>` (parseJsx.ts assertNoClassNameSections). */
|
|
228
|
+
function classNameSection(at, property, isEntry) {
|
|
229
|
+
const key = getKey(property);
|
|
230
|
+
const removed = key === 'visibility' && !isEntry && getObject(property.value) && getProperty(getObject(property.value), REMOVED);
|
|
231
|
+
if (removed)
|
|
232
|
+
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');
|
|
233
|
+
const term = key === 'states' ? '{ hover: is(…) }' : "{ 'wix-visibility-hidden': is(…) }";
|
|
234
|
+
const fix = isEntry ? `in the entry's \`className: ${term}\`` : `as a \`className\` term: \`className={['comp-x', ${term}]}\``;
|
|
235
|
+
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');
|
|
236
|
+
}
|
|
237
|
+
function section(at, name, node) {
|
|
203
238
|
const fix = `Write an object of \`key: value\` entries: \`${name}: { label: … }\`.`;
|
|
204
239
|
const value = bare(node);
|
|
205
240
|
if (value.type !== 'ObjectExpression')
|
|
@@ -216,23 +251,36 @@ function section(at, name, node, isEntry) {
|
|
|
216
251
|
say(at, property, `\`bindings.${name}\` on ${at.owner} ${problem}. ${fix}`, 'CONV-parseJsx.ts:669');
|
|
217
252
|
continue;
|
|
218
253
|
}
|
|
254
|
+
prop(at, getKey(property), property.value, false);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** `className={['comp-x', 'open', { hover: is(…) }]}`: each object term's key holds a condition, as `bindings.states` and `bindings.visibility` did (parseJsx.ts mergeClassName). */
|
|
258
|
+
function classNameAttribute(at, attribute) {
|
|
259
|
+
const value = getExpression(attribute);
|
|
260
|
+
if (value?.type !== 'ArrayExpression')
|
|
261
|
+
return;
|
|
262
|
+
for (const term of value.elements.slice(1)) {
|
|
263
|
+
if (term?.type === 'ObjectExpression')
|
|
264
|
+
classNameConditions(at, term, '`className`');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function classNameConditions(at, terms, where) {
|
|
268
|
+
for (const property of getProperties(terms)) {
|
|
219
269
|
const key = getKey(property);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
else if (name === 'visibility' && key !== REMOVED && !VISIBILITY_KEYS.includes(key))
|
|
226
|
-
say(at, property, `\`bindings.visibility\` on ${at.owner} has the key \`${key}\`. Use \`collapsed\` or \`hidden\`.`, 'CONV-decode.ts:221');
|
|
270
|
+
if (key === null)
|
|
271
|
+
continue;
|
|
272
|
+
if (key.startsWith(VISIBILITY_PREFIX) && !VISIBILITY_KEYS.includes(key.slice(VISIBILITY_PREFIX.length))) {
|
|
273
|
+
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');
|
|
274
|
+
}
|
|
227
275
|
else
|
|
228
|
-
condition(at, property.value, where);
|
|
276
|
+
condition(at, property.value, `${where} term \`${key}\``);
|
|
229
277
|
}
|
|
230
278
|
}
|
|
231
279
|
function prop(at, name, node, placeholderAllowed) {
|
|
232
280
|
const value = bare(node);
|
|
233
281
|
if (value.type === 'ArrayExpression')
|
|
234
282
|
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');
|
|
235
|
-
if (isPlain(value) || rawOnly(value) || isStaticRaw(at, value))
|
|
283
|
+
if (isPlain(value) || rawOnly(value) || isStaticRaw(at, value) || isTyped(value, at.input.rulesApi))
|
|
236
284
|
return say(at, value, fixedValue(at, name), 'CONV-decode.ts:252');
|
|
237
285
|
binding(at, value, 'prop', placeholderAllowed);
|
|
238
286
|
}
|
|
@@ -255,10 +303,17 @@ function condition(at, node, where, inRemove = false) {
|
|
|
255
303
|
function binding(at, node, place, placeholderAllowed = false) {
|
|
256
304
|
const value = bare(node);
|
|
257
305
|
const { name } = PLACES[place];
|
|
306
|
+
// A read of the renderItem row or of a map() item (`item.name`) is a binding in every place (decode.ts decodeRef).
|
|
307
|
+
if (getParamReadKind(value))
|
|
308
|
+
return;
|
|
309
|
+
if (isInvoke(value))
|
|
310
|
+
return invoke(at, value);
|
|
258
311
|
if (isCallByName(value)) {
|
|
259
312
|
const builder = builderOf(value, at.input.rulesApi);
|
|
260
313
|
if (!builder)
|
|
261
314
|
return;
|
|
315
|
+
if (TYPED_BUILDERS.includes(builder))
|
|
316
|
+
return PLACES[place].literals ? undefined : wrongPlace(at, value, `\`${builder}()\``, place);
|
|
262
317
|
if (!PLACES[place].builders.includes(builder))
|
|
263
318
|
return wrongPlace(at, value, `\`${builder}()\``, place);
|
|
264
319
|
return call(at, value, builder, place, placeholderAllowed);
|
|
@@ -269,31 +324,46 @@ function binding(at, node, place, placeholderAllowed = false) {
|
|
|
269
324
|
return;
|
|
270
325
|
}
|
|
271
326
|
const raw = rawOnly(value);
|
|
272
|
-
if (raw && place === '
|
|
273
|
-
return
|
|
327
|
+
if (raw === 'an object' && place === 'fnArg' && isDataItem(value))
|
|
328
|
+
return;
|
|
329
|
+
if (raw === 'an object' && place === 'fnArg')
|
|
330
|
+
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');
|
|
274
331
|
if (raw)
|
|
275
332
|
return say(at, value, `${capitalize(name)} on ${at.owner} is ${raw}. Write ${PLAIN}, or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
|
|
276
333
|
say(at, value, `${capitalize(name)} on ${at.owner} is ${describeIn(at, value)}. Write it out. ${hintFor(place)}`, 'CONV-decode.ts:86');
|
|
277
334
|
}
|
|
278
|
-
const wrongPlace = (at, node, what, place) => say(at, node, `${capitalize(what)} can't be ${PLACES[place].name} on ${at.owner}. ${hintFor(place)}`,
|
|
335
|
+
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');
|
|
336
|
+
/** A called ctx() or row member: `ctx('cart.add', { from: useCart })(item.id)`, `item.actions.select('quick')` (decode.ts decodeInvoke). */
|
|
337
|
+
function invoke(at, node) {
|
|
338
|
+
const { callee } = node;
|
|
339
|
+
if (!isMethodTarget(callee, at.input.rulesApi))
|
|
340
|
+
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');
|
|
341
|
+
if (callee.type === 'CallExpression')
|
|
342
|
+
ctxCall(at, callee, false);
|
|
343
|
+
if (node.arguments.some((argument) => argument.type === 'SpreadElement'))
|
|
344
|
+
return say(at, node, `A called \`ctx()\` or row member on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
|
|
345
|
+
for (const argument of node.arguments)
|
|
346
|
+
binding(at, argument, 'fnArg');
|
|
347
|
+
}
|
|
279
348
|
function call(at, node, builder, place, placeholderAllowed) {
|
|
280
349
|
if (node.arguments.some((argument) => argument.type === 'SpreadElement')) {
|
|
281
350
|
return say(at, node, `\`${builder}()\` on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
|
|
282
351
|
}
|
|
283
|
-
|
|
352
|
+
// map() and shape() hold a callback, read in its own scope: no tracker row checks its shape yet.
|
|
353
|
+
if (builder === 'map' || builder === 'shape' || hasDuplicateKey(at, node, builder))
|
|
284
354
|
return;
|
|
285
355
|
if (builder === 'ctx')
|
|
286
356
|
ctxCall(at, node, placeholderAllowed);
|
|
287
357
|
else if (builder === 'fn')
|
|
288
358
|
fnCall(at, node, placeholderAllowed);
|
|
359
|
+
else if (builder === 'callback')
|
|
360
|
+
callbackCall(at, node);
|
|
289
361
|
else if (builder === 'is')
|
|
290
362
|
isCall(at, node);
|
|
291
363
|
else if (builder === 'all')
|
|
292
364
|
allCall(at, node, place);
|
|
293
|
-
else if (builder === 'raw')
|
|
294
|
-
isRawWrittenOut(at, node);
|
|
295
365
|
else
|
|
296
|
-
|
|
366
|
+
isRawWrittenOut(at, node);
|
|
297
367
|
}
|
|
298
368
|
function hasDuplicateKey(at, node, builder) {
|
|
299
369
|
const twice = node.arguments.map(duplicateKeyIn).find(Boolean);
|
|
@@ -334,6 +404,21 @@ function fnCall(at, node, placeholderAllowed) {
|
|
|
334
404
|
binding(at, argument, 'fnArg');
|
|
335
405
|
}
|
|
336
406
|
}
|
|
407
|
+
/** `callback(goHome, 2)`: the function (an imported one, a `ctx()` or a member of the row), then its bound arguments (decode.ts decodeCallback). */
|
|
408
|
+
function callbackCall(at, node) {
|
|
409
|
+
const [first, ...bound] = node.arguments;
|
|
410
|
+
if (!first)
|
|
411
|
+
return say(at, node, `\`callback()\` on ${at.owner} has no arguments. Write the function first: \`callback(goHome, …)\`.`, 'CONV-astValue.ts:287');
|
|
412
|
+
const target = bare(first);
|
|
413
|
+
// A name that isn't a parameter is the imported function: bindingNameImported checks its import.
|
|
414
|
+
const isFunctionName = target.type === 'Identifier' && !getParamReadKind(target);
|
|
415
|
+
if (!isFunctionName && !isMethodTarget(target, at.input.rulesApi))
|
|
416
|
+
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');
|
|
417
|
+
if (target.type === 'CallExpression')
|
|
418
|
+
ctxCall(at, target, false);
|
|
419
|
+
for (const argument of bound)
|
|
420
|
+
binding(at, argument, 'fnArg');
|
|
421
|
+
}
|
|
337
422
|
function placeholderValue(at, node) {
|
|
338
423
|
const value = bare(node);
|
|
339
424
|
if (!isPlain(value))
|
|
@@ -344,14 +429,26 @@ function isCall(at, node) {
|
|
|
344
429
|
if (node.arguments.length !== 2)
|
|
345
430
|
return say(at, node, `\`is()\` on ${at.owner} has ${node.arguments.length} argument${node.arguments.length === 1 ? '' : 's'}. ${fix}`, 'CONV-decode.ts:172');
|
|
346
431
|
const [binded, compared] = node.arguments.map((argument) => bare(argument));
|
|
347
|
-
if (isPlain(binded) || rawOnly(binded) || builderOf(binded, at.input.rulesApi) === 'raw')
|
|
348
|
-
say(at, binded, `\`is()\` on ${at.owner} compares ${isCallByName(binded) ?
|
|
432
|
+
if (isPlain(binded) || rawOnly(binded) || builderOf(binded, at.input.rulesApi) === 'raw' || isTyped(binded, at.input.rulesApi))
|
|
433
|
+
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');
|
|
349
434
|
else
|
|
350
435
|
binding(at, binded, 'isBinding');
|
|
351
436
|
const isStringOrBoolean = compared.type === 'Literal' && ['string', 'boolean'].includes(typeof compared.value);
|
|
352
437
|
if (!isStringOrBoolean)
|
|
353
438
|
say(at, compared, `\`is()\` on ${at.owner} compares to ${rawOnly(compared) ?? describeIn(at, compared)}. ${fix}`, 'CONV-decode.ts:172');
|
|
354
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* A component's `raw()` bindings are one item of the bindings namespace, and its one schema is BindingsList (reservedAttributes.ts
|
|
442
|
+
* BINDINGS_WRAPPER_TYPE): the import refuses an item with no type, or another one ("Cannot find schema … for namespace: bindings").
|
|
443
|
+
* An elementProps entry's bindings are stored inline in the component's inner elements, so they have no such item.
|
|
444
|
+
*/
|
|
445
|
+
function checkRawBindingsType(at, node, item) {
|
|
446
|
+
const type = getProperty(item, 'type');
|
|
447
|
+
if (stringValue(type?.value) === BINDINGS_LIST)
|
|
448
|
+
return;
|
|
449
|
+
const has = type ? `has the type \`${textOf(at, type.value)}\`` : 'has no `type`';
|
|
450
|
+
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');
|
|
451
|
+
}
|
|
355
452
|
function allCall(at, node, place) {
|
|
356
453
|
const count = node.arguments.length;
|
|
357
454
|
if (place === 'condition' && count < 2)
|
|
@@ -379,61 +476,30 @@ function isRawWrittenOut(at, node) {
|
|
|
379
476
|
say(at, found, `\`raw()\` on ${at.owner} holds ${describeIn(at, found)}. Write its item out with literal values.`, 'CONV-decode.ts:28');
|
|
380
477
|
return !found;
|
|
381
478
|
}
|
|
382
|
-
|
|
383
|
-
const [nameNode, options, ...extra] = node.arguments;
|
|
384
|
-
const name = stringValue(nameNode);
|
|
385
|
-
const object = options && getObject(options);
|
|
386
|
-
const keys = object ? getProperties(object).map(getKey) : [];
|
|
387
|
-
const unknown = keys.find((key) => key !== null && !STATE_OPTIONS.includes(key));
|
|
388
|
-
const problem = name === null ? 'has no name string first'
|
|
389
|
-
: extra.length > 0 ? 'has more than two arguments'
|
|
390
|
-
: !object || object.properties.length !== keys.length ? "has no options object, or one that isn't all `key: value`"
|
|
391
|
-
: unknown !== undefined ? `has the option \`${unknown}\``
|
|
392
|
-
: null;
|
|
393
|
-
if (problem)
|
|
394
|
-
return say(at, node, `\`state()\` on ${at.owner} ${problem}. Write \`state('hover', { kind: 'hover', id: 'state-1' })\` (pseudoClass is optional).`, 'CONV-decode.ts:145');
|
|
395
|
-
const found = nonLiteralIn(object);
|
|
396
|
-
if (found)
|
|
397
|
-
return say(at, found, `\`state()\` on ${at.owner} holds ${describeIn(at, found)}. Write its options out with literal values.`, 'CONV-decode.ts:145');
|
|
398
|
-
if (stringValue(getProperty(object, 'kind')?.value) === null)
|
|
399
|
-
return say(at, node, `\`state()\` on ${at.owner} has no \`kind\` string. Write \`state('hover', { kind: 'hover', id: 'state-1' })\` (pseudoClass is optional).`, 'CONV-decode.ts:145');
|
|
400
|
-
if (stringValue(getProperty(object, 'id')?.value) === null)
|
|
401
|
-
say(at, node, `\`state('${name}')\` on ${at.owner} has no \`id\` string. Add \`id: '…'\` to its options.`, 'CONV-decode.ts:142');
|
|
402
|
-
}
|
|
403
|
-
function statesAttribute(at, attribute) {
|
|
404
|
-
if (!attribute)
|
|
405
|
-
return;
|
|
406
|
-
const value = getExpression(attribute);
|
|
407
|
-
if (value?.type === 'ArrayExpression' && !hasHoleOrSpread(value)) {
|
|
408
|
-
for (const item of value.elements)
|
|
409
|
-
binding(at, item, 'statesItem');
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
const what = value?.type === 'ArrayExpression' ? 'a list with a spread or an empty place' : value ? describeIn(at, value) : 'not a `{…}` value';
|
|
413
|
-
say(at, attribute, `\`states\` on ${at.owner} is ${what}. Write a list of \`state()\` or \`raw()\` calls: \`states={[state('hover', {…})]}\`.`, 'CONV-parseJsx.ts:641');
|
|
414
|
-
}
|
|
415
|
-
/** Only RUNTIME_ONLY_BUILDERS: other runtime names (`Remove(…)`) pass the converter's parse, then its decode has no case for them. */
|
|
479
|
+
/** Only BUILDERS: other runtime names (`Remove(…)`) pass the converter's parse, then its decode has no case for them. */
|
|
416
480
|
function isBuilderImported({ rulesApi, report }, callee) {
|
|
417
481
|
const found = rulesApi.getImport(callee);
|
|
418
|
-
if (found?.kind === 'runtime' && found.imported === callee.name &&
|
|
482
|
+
if (found?.kind === 'runtime' && found.imported === callee.name && BUILDERS.includes(callee.name))
|
|
419
483
|
return true;
|
|
420
484
|
const builder = found?.kind === 'runtime' ? found.imported : callee.name;
|
|
421
485
|
report({
|
|
422
486
|
loc: callee.loc,
|
|
423
|
-
message: tracked(
|
|
487
|
+
message: tracked(BUILDERS.includes(builder)
|
|
424
488
|
? `\`${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}(…)\`.`
|
|
425
|
-
: `\`${callee.name}(…)\` isn't a builder. In an attribute, only the builders of ${RUNTIME_PACKAGE} are called (${
|
|
489
|
+
: `\`${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'),
|
|
426
490
|
});
|
|
427
491
|
return false;
|
|
428
492
|
}
|
|
429
493
|
function bindingNameImported({ node, rulesApi, report, context }, builder) {
|
|
430
494
|
const [first, second] = node.arguments;
|
|
431
495
|
const options = builder === 'ctx' ? getObject(second) : null;
|
|
432
|
-
|
|
496
|
+
// callback()'s first argument may also be a `ctx()` or a member of the row (astValue.ts astToCallArg).
|
|
497
|
+
const isCallbackName = builder === 'callback' && first && bare(first).type === 'Identifier' && !getParamReadKind(bare(first));
|
|
498
|
+
const named = builder === 'fn' || isCallbackName ? first : options ? getProperty(options, 'from')?.value : undefined;
|
|
433
499
|
if (!named)
|
|
434
500
|
return null;
|
|
435
501
|
const value = bare(named);
|
|
436
|
-
const what = builder === 'fn' ? "fn()'s function" : "ctx()'s `from` hook";
|
|
502
|
+
const what = builder === 'fn' ? "fn()'s function" : builder === 'callback' ? "callback()'s function" : "ctx()'s `from` hook";
|
|
437
503
|
if (value.type !== 'Identifier') {
|
|
438
504
|
report({ loc: value.loc, message: tracked(`${what} is \`${context.sourceCode.getText(value)}\`. Write an imported name: \`fn(numberToText, …)\`, \`{ from: useProduct }\`.`, 'CONV-astValue.ts:227') });
|
|
439
505
|
return null;
|
|
@@ -456,12 +522,6 @@ function ctxSourceHasName({ report }, { hook, source }) {
|
|
|
456
522
|
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') });
|
|
457
523
|
}
|
|
458
524
|
// ── reading the code ──
|
|
459
|
-
function getObject(source) {
|
|
460
|
-
if (!source)
|
|
461
|
-
return null;
|
|
462
|
-
const value = source.type === 'JSXAttribute' ? getExpression(source) : bare(source);
|
|
463
|
-
return value?.type === 'ObjectExpression' ? value : null;
|
|
464
|
-
}
|
|
465
525
|
function getElementPropsEntries(element) {
|
|
466
526
|
return getEntries(getObject(getAttribute(element, 'elementProps')));
|
|
467
527
|
}
|
|
@@ -478,9 +538,22 @@ function builderOf(node, rulesApi) {
|
|
|
478
538
|
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier')
|
|
479
539
|
return null;
|
|
480
540
|
const found = rulesApi.getImport(node.callee);
|
|
481
|
-
return found?.kind === 'runtime' &&
|
|
541
|
+
return found?.kind === 'runtime' && BUILDERS.includes(found.imported) ? found.imported : null;
|
|
482
542
|
}
|
|
483
543
|
const isCallByName = (node) => node.type === 'CallExpression' && node.callee.type === 'Identifier';
|
|
544
|
+
/** What the import calls as a method (decode.ts methodBindingOf): a `ctx()`, or a read of the renderItem row. A map() or shape() parameter has no methods. */
|
|
545
|
+
const isMethodTarget = (node, rulesApi) => builderOf(node, rulesApi) === 'ctx' || getParamReadKind(node) === 'template';
|
|
546
|
+
/** A call of a call or of a member: `ctx(…)(…)`, `item.actions.select()`. */
|
|
547
|
+
const isInvoke = (node) => node.type === 'CallExpression' && (node.callee.type === 'CallExpression' || node.callee.type === 'MemberExpression');
|
|
548
|
+
/** A value the import reads as a binding: a builder call, a called ctx() or row member, a read of a renderItem or map() parameter. */
|
|
549
|
+
const isBindingValue = (node, rulesApi) => builderOf(node, rulesApi) !== null || isInvoke(node) || getParamReadKind(node) !== null;
|
|
550
|
+
/** A typed builder, `email('a@b.c')`: a fixed value, like a literal (decode.ts decodeTypedStatic). */
|
|
551
|
+
const isTyped = (node, rulesApi) => TYPED_BUILDERS.includes(builderOf(node, rulesApi) ?? '');
|
|
552
|
+
/** An argument's object the import reads as a data item: a typed Builder item or link, or a link with an `href` and no type (decode.ts decodeDataRef). */
|
|
553
|
+
function isDataItem(item) {
|
|
554
|
+
const type = getProperty(item, 'type');
|
|
555
|
+
return type ? DATA_ITEM_TYPES.includes(stringValue(type.value) ?? '') : getProperty(item, 'href') !== undefined;
|
|
556
|
+
}
|
|
484
557
|
function isPlain(node) {
|
|
485
558
|
if (node.type === 'UnaryExpression')
|
|
486
559
|
return node.operator === '-' && node.argument.type === 'Literal' && typeof node.argument.value === 'number';
|
|
@@ -547,22 +620,45 @@ const wrappedComponents = (element, rulesApi) => rulesApi.getChildren(element).f
|
|
|
547
620
|
return wrappedComponents(child, rulesApi);
|
|
548
621
|
return rulesApi.getComponent(child) ? [child] : [];
|
|
549
622
|
});
|
|
550
|
-
/** The
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
623
|
+
/** The runtime's withOrigami wraps the component wrappers and the companion components: only on them does a callback prop throw. */
|
|
624
|
+
function isWrappedByRuntime(element, rulesApi) {
|
|
625
|
+
const kind = rulesApi.getImport(element)?.kind;
|
|
626
|
+
return kind === 'wrapper' || kind === 'siteCode';
|
|
627
|
+
}
|
|
628
|
+
/** A callback prop written as an attribute with a call: `onClick={fn(goHome)}`. */
|
|
629
|
+
function getCallbackAttributes(element) {
|
|
630
|
+
return element.openingElement.attributes.flatMap((attribute) => attribute.type === 'JSXAttribute' && CALLBACK_PROP.test(getName(attribute)) && getExpression(attribute)?.type === 'CallExpression' ? [[getName(attribute), attribute]] : []);
|
|
631
|
+
}
|
|
632
|
+
/** The callback props in `bindings={{ props: { onClick: … } }}`. */
|
|
633
|
+
function getBoundCallbacks(attribute) {
|
|
634
|
+
const bindings = getObject(attribute);
|
|
635
|
+
const props = bindings && getObject(getProperty(bindings, BINDINGS_KEY)?.value);
|
|
636
|
+
return (props ? getProperties(props) : []).flatMap((property) => {
|
|
637
|
+
const key = getKey(property);
|
|
638
|
+
return key !== null && CALLBACK_PROP.test(key) ? [[key, bare(property.value)]] : [];
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
/** What a callback prop is bound with that runs at render: `fn()`, `ctx()`, a called ctx() or row member, a read of the row; null for `callback()` and `raw()`. */
|
|
642
|
+
function runsAtRender(at, node) {
|
|
643
|
+
const builder = builderOf(node, at.input.rulesApi);
|
|
644
|
+
if (builder)
|
|
645
|
+
return RENDER_BUILDERS.includes(builder) ? `\`${builder}()\`` : null;
|
|
646
|
+
return isInvoke(node) || getParamReadKind(node) ? describeIn(at, node) : null;
|
|
647
|
+
}
|
|
648
|
+
/** 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). */
|
|
554
649
|
function boundAttributes(element) {
|
|
555
650
|
return element.openingElement.attributes.flatMap((attribute) => {
|
|
556
|
-
if (attribute.type !== 'JSXAttribute' ||
|
|
651
|
+
if (attribute.type !== 'JSXAttribute' || isReservedAttribute(getName(attribute)) || EVENT_ATTRIBUTE.test(getName(attribute)))
|
|
557
652
|
return [];
|
|
558
653
|
const value = getExpression(attribute);
|
|
559
|
-
return value
|
|
654
|
+
return isBound(value) ? [[getName(attribute), attribute, value]] : [];
|
|
560
655
|
});
|
|
561
656
|
}
|
|
562
657
|
function boundEntryKeys(entry) {
|
|
563
658
|
return getProperties(entry).flatMap((property) => {
|
|
564
659
|
const key = getKey(property);
|
|
565
|
-
|
|
660
|
+
const value = bare(property.value);
|
|
661
|
+
return key !== null && !RESERVED_ENTRY_KEYS.includes(key) && isBound(value) ? [[key, property]] : [];
|
|
566
662
|
});
|
|
567
663
|
}
|
|
568
664
|
function getBindingsProps(node, rulesApi) {
|
|
@@ -617,13 +713,14 @@ function misplacedBuilders(element, rulesApi) {
|
|
|
617
713
|
return [];
|
|
618
714
|
const name = getName(attribute);
|
|
619
715
|
const value = getExpression(attribute);
|
|
620
|
-
if (!value || name === 'bindings'
|
|
716
|
+
if (!value || name === 'bindings')
|
|
621
717
|
return [];
|
|
622
718
|
if (name === 'elementProps')
|
|
623
719
|
return entryBuilders(value, rulesApi, '`elementProps`');
|
|
624
|
-
if (!
|
|
720
|
+
if (!isReservedAttribute(name) && isBindingValue(value, rulesApi))
|
|
625
721
|
return [];
|
|
626
|
-
|
|
722
|
+
const builders = name === 'className' && value.type === 'ArrayExpression' ? value.elements.flatMap((term) => conditionlessBuilders(term, rulesApi)) : buildersIn(value, rulesApi);
|
|
723
|
+
return builders.map((call) => [call, `\`${name}\``]);
|
|
627
724
|
});
|
|
628
725
|
}
|
|
629
726
|
function entryBuilders(elementProps, rulesApi, where) {
|
|
@@ -643,12 +740,19 @@ function entryBuilders(elementProps, rulesApi, where) {
|
|
|
643
740
|
return [];
|
|
644
741
|
if (key === 'elementProps')
|
|
645
742
|
return entryBuilders(property.value, rulesApi, `${name}'s \`elementProps\``);
|
|
646
|
-
if (!RESERVED_ENTRY_KEYS.includes(key) &&
|
|
743
|
+
if (!RESERVED_ENTRY_KEYS.includes(key) && isBindingValue(bare(property.value), rulesApi))
|
|
647
744
|
return [];
|
|
648
|
-
|
|
745
|
+
const builders = key === 'className' ? conditionlessBuilders(property.value, rulesApi) : buildersIn(property.value, rulesApi);
|
|
746
|
+
return builders.map((call) => [call, `${name}'s \`${key}\``]);
|
|
649
747
|
});
|
|
650
748
|
});
|
|
651
749
|
}
|
|
750
|
+
/** The builders in a `className` term outside its keys' conditions: an object term's `key: value` holds a condition, the rest is plain (parseJsx.ts classNameEntriesOf). */
|
|
751
|
+
function conditionlessBuilders(term, rulesApi) {
|
|
752
|
+
if (term?.type !== 'ObjectExpression')
|
|
753
|
+
return buildersIn(term, rulesApi);
|
|
754
|
+
return term.properties.flatMap((property) => property.type === 'Property' && !property.computed && getKey(property) !== null ? [] : buildersIn(property, rulesApi));
|
|
755
|
+
}
|
|
652
756
|
function buildersIn(node, rulesApi) {
|
|
653
757
|
if (!node || typeof node !== 'object' || !('type' in node))
|
|
654
758
|
return [];
|
|
@@ -666,6 +770,9 @@ function isInStructureAttribute(node) {
|
|
|
666
770
|
for (let up = node.parent; up.type !== 'Program'; up = up.parent) {
|
|
667
771
|
if (up.type === 'JSXAttribute')
|
|
668
772
|
inAttribute = true;
|
|
773
|
+
// A renderItem template and a map() or shape() callback are read in the structure, in their parameters' scope.
|
|
774
|
+
if (up.type === 'ArrowFunctionExpression' && getCallbackKind(up))
|
|
775
|
+
continue;
|
|
669
776
|
if (up.type === 'ArrowFunctionExpression' || up.type === 'FunctionExpression')
|
|
670
777
|
return inAttribute && isStructure(up);
|
|
671
778
|
}
|