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