@tenphi/eslint-plugin-tasty 0.1.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/_virtual/_rolldown/runtime.mjs +7 -0
- package/dist/config.mjs +118 -0
- package/dist/config.mjs.map +1 -0
- package/dist/configs.d.mts +8 -0
- package/dist/configs.mjs +36 -0
- package/dist/configs.mjs.map +1 -0
- package/dist/constants.mjs +700 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/context.mjs +180 -0
- package/dist/context.mjs.map +1 -0
- package/dist/create-rule.mjs +8 -0
- package/dist/create-rule.mjs.map +1 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +83 -0
- package/dist/index.mjs.map +1 -0
- package/dist/parser.mjs +46 -0
- package/dist/parser.mjs.map +1 -0
- package/dist/property-expectations.mjs +168 -0
- package/dist/property-expectations.mjs.map +1 -0
- package/dist/rules/consistent-token-usage.mjs +88 -0
- package/dist/rules/consistent-token-usage.mjs.map +1 -0
- package/dist/rules/known-property.mjs +55 -0
- package/dist/rules/known-property.mjs.map +1 -0
- package/dist/rules/no-duplicate-state.mjs +51 -0
- package/dist/rules/no-duplicate-state.mjs.map +1 -0
- package/dist/rules/no-important.mjs +44 -0
- package/dist/rules/no-important.mjs.map +1 -0
- package/dist/rules/no-nested-selector.mjs +40 -0
- package/dist/rules/no-nested-selector.mjs.map +1 -0
- package/dist/rules/no-nested-state-map.mjs +44 -0
- package/dist/rules/no-nested-state-map.mjs.map +1 -0
- package/dist/rules/no-raw-color-values.mjs +84 -0
- package/dist/rules/no-raw-color-values.mjs.map +1 -0
- package/dist/rules/no-runtime-styles-mutation.mjs +52 -0
- package/dist/rules/no-runtime-styles-mutation.mjs.map +1 -0
- package/dist/rules/no-styles-prop.mjs +25 -0
- package/dist/rules/no-styles-prop.mjs.map +1 -0
- package/dist/rules/no-unknown-state-alias.mjs +49 -0
- package/dist/rules/no-unknown-state-alias.mjs.map +1 -0
- package/dist/rules/prefer-shorthand-property.mjs +45 -0
- package/dist/rules/prefer-shorthand-property.mjs.map +1 -0
- package/dist/rules/require-default-state.mjs +47 -0
- package/dist/rules/require-default-state.mjs.map +1 -0
- package/dist/rules/static-no-dynamic-values.mjs +54 -0
- package/dist/rules/static-no-dynamic-values.mjs.map +1 -0
- package/dist/rules/static-valid-selector.mjs +51 -0
- package/dist/rules/static-valid-selector.mjs.map +1 -0
- package/dist/rules/valid-boolean-property.mjs +45 -0
- package/dist/rules/valid-boolean-property.mjs.map +1 -0
- package/dist/rules/valid-color-token.mjs +84 -0
- package/dist/rules/valid-color-token.mjs.map +1 -0
- package/dist/rules/valid-custom-property.mjs +69 -0
- package/dist/rules/valid-custom-property.mjs.map +1 -0
- package/dist/rules/valid-custom-unit.mjs +62 -0
- package/dist/rules/valid-custom-unit.mjs.map +1 -0
- package/dist/rules/valid-directional-modifier.mjs +71 -0
- package/dist/rules/valid-directional-modifier.mjs.map +1 -0
- package/dist/rules/valid-preset.mjs +64 -0
- package/dist/rules/valid-preset.mjs.map +1 -0
- package/dist/rules/valid-radius-shape.mjs +77 -0
- package/dist/rules/valid-radius-shape.mjs.map +1 -0
- package/dist/rules/valid-recipe.mjs +51 -0
- package/dist/rules/valid-recipe.mjs.map +1 -0
- package/dist/rules/valid-state-key.mjs +71 -0
- package/dist/rules/valid-state-key.mjs.map +1 -0
- package/dist/rules/valid-styles-structure.mjs +79 -0
- package/dist/rules/valid-styles-structure.mjs.map +1 -0
- package/dist/rules/valid-sub-element.mjs +46 -0
- package/dist/rules/valid-sub-element.mjs.map +1 -0
- package/dist/rules/valid-transition.mjs +62 -0
- package/dist/rules/valid-transition.mjs.map +1 -0
- package/dist/rules/valid-value.mjs +123 -0
- package/dist/rules/valid-value.mjs.map +1 -0
- package/dist/types.d.mts +25 -0
- package/dist/utils.mjs +152 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { PRESET_MODIFIERS } from "../constants.mjs";
|
|
3
|
+
import { TastyContext } from "../context.mjs";
|
|
4
|
+
import { getKeyName, getStringValue } from "../utils.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/rules/valid-preset.ts
|
|
7
|
+
var valid_preset_default = createRule({
|
|
8
|
+
name: "valid-preset",
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: { description: "Validate preset property values against config" },
|
|
12
|
+
messages: {
|
|
13
|
+
unknownPreset: "Unknown preset '{{name}}'.",
|
|
14
|
+
unknownModifier: "Unknown preset modifier '{{modifier}}'."
|
|
15
|
+
},
|
|
16
|
+
schema: []
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
const ctx = new TastyContext(context);
|
|
21
|
+
function checkPresetValue(value, node) {
|
|
22
|
+
const parts = value.trim().split(/\s+/);
|
|
23
|
+
if (parts.length === 0) return;
|
|
24
|
+
const [presetName, ...modifiers] = parts;
|
|
25
|
+
if (ctx.config.presets.length > 0 && !ctx.config.presets.includes(presetName)) context.report({
|
|
26
|
+
node,
|
|
27
|
+
messageId: "unknownPreset",
|
|
28
|
+
data: { name: presetName }
|
|
29
|
+
});
|
|
30
|
+
for (const mod of modifiers) if (!PRESET_MODIFIERS.has(mod)) context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: "unknownModifier",
|
|
33
|
+
data: { modifier: mod }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
ImportDeclaration(node) {
|
|
38
|
+
ctx.trackImport(node);
|
|
39
|
+
},
|
|
40
|
+
"CallExpression ObjectExpression"(node) {
|
|
41
|
+
if (!ctx.isStyleObject(node)) return;
|
|
42
|
+
for (const prop of node.properties) {
|
|
43
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
44
|
+
if (getKeyName(prop.key) !== "preset") continue;
|
|
45
|
+
const str = getStringValue(prop.value);
|
|
46
|
+
if (str) {
|
|
47
|
+
checkPresetValue(str, prop.value);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (prop.value.type === "Literal" && prop.value.value === true) continue;
|
|
51
|
+
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
52
|
+
if (stateProp.type !== "Property") continue;
|
|
53
|
+
const stateStr = getStringValue(stateProp.value);
|
|
54
|
+
if (stateStr) checkPresetValue(stateStr, stateProp.value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { valid_preset_default as default };
|
|
64
|
+
//# sourceMappingURL=valid-preset.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-preset.mjs","names":[],"sources":["../../src/rules/valid-preset.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { PRESET_MODIFIERS } from '../constants.js';\n\ntype MessageIds = 'unknownPreset' | 'unknownModifier';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-preset',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate preset property values against config',\n },\n messages: {\n unknownPreset: \"Unknown preset '{{name}}'.\",\n unknownModifier: \"Unknown preset modifier '{{modifier}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkPresetValue(value: string, node: TSESTree.Node): void {\n const parts = value.trim().split(/\\s+/);\n if (parts.length === 0) return;\n\n const [presetName, ...modifiers] = parts;\n\n if (\n ctx.config.presets.length > 0 &&\n !ctx.config.presets.includes(presetName)\n ) {\n context.report({\n node,\n messageId: 'unknownPreset',\n data: { name: presetName },\n });\n }\n\n for (const mod of modifiers) {\n if (!PRESET_MODIFIERS.has(mod)) {\n context.report({\n node,\n messageId: 'unknownModifier',\n data: { modifier: mod },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key !== 'preset') continue;\n\n // Direct string value\n const str = getStringValue(prop.value);\n if (str) {\n checkPresetValue(str, prop.value);\n continue;\n }\n\n // true is always valid\n if (prop.value.type === 'Literal' && prop.value.value === true) {\n continue;\n }\n\n // State map\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkPresetValue(stateStr, stateProp.value);\n }\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,kDACd;EACD,UAAU;GACR,eAAe;GACf,iBAAiB;GAClB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,iBAAiB,OAAe,MAA2B;GAClE,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM;AACvC,OAAI,MAAM,WAAW,EAAG;GAExB,MAAM,CAAC,YAAY,GAAG,aAAa;AAEnC,OACE,IAAI,OAAO,QAAQ,SAAS,KAC5B,CAAC,IAAI,OAAO,QAAQ,SAAS,WAAW,CAExC,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,YAAY;IAC3B,CAAC;AAGJ,QAAK,MAAM,OAAO,UAChB,KAAI,CAAC,iBAAiB,IAAI,IAAI,CAC5B,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,UAAU,KAAK;IACxB,CAAC;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,SADY,WAAW,KAAK,IAAI,KACpB,SAAU;KAGtB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,KAAK;AACP,uBAAiB,KAAK,KAAK,MAAM;AACjC;;AAIF,SAAI,KAAK,MAAM,SAAS,aAAa,KAAK,MAAM,UAAU,KACxD;AAIF,SAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,UAAI,UAAU,SAAS,WAAY;MACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,UAAI,SACF,kBAAiB,UAAU,UAAU,MAAM;;;;GAMtD;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { RADIUS_SHAPES } from "../constants.mjs";
|
|
3
|
+
import { TastyContext } from "../context.mjs";
|
|
4
|
+
import { getKeyName, getStringValue } from "../utils.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/rules/valid-radius-shape.ts
|
|
7
|
+
const SHAPE_LIKE = /^[a-z]+$/;
|
|
8
|
+
var valid_radius_shape_default = createRule({
|
|
9
|
+
name: "valid-radius-shape",
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: { description: "Validate special shape keywords used with the radius property" },
|
|
13
|
+
messages: { unknownShape: "Unknown radius shape '{{shape}}'. Valid shapes: {{valid}}." },
|
|
14
|
+
schema: []
|
|
15
|
+
},
|
|
16
|
+
defaultOptions: [],
|
|
17
|
+
create(context) {
|
|
18
|
+
const ctx = new TastyContext(context);
|
|
19
|
+
function checkRadiusValue(value, node) {
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (!SHAPE_LIKE.test(trimmed)) return;
|
|
22
|
+
if (RADIUS_SHAPES.has(trimmed)) return;
|
|
23
|
+
if (trimmed === "true" || trimmed === "false") return;
|
|
24
|
+
if (trimmed === "none" || trimmed === "inherit" || trimmed === "initial") return;
|
|
25
|
+
if (new Set([
|
|
26
|
+
"top",
|
|
27
|
+
"right",
|
|
28
|
+
"bottom",
|
|
29
|
+
"left",
|
|
30
|
+
"top-left",
|
|
31
|
+
"top-right",
|
|
32
|
+
"bottom-left",
|
|
33
|
+
"bottom-right"
|
|
34
|
+
]).has(trimmed)) return;
|
|
35
|
+
const suggestion = findClosestShape(trimmed);
|
|
36
|
+
const validList = [...RADIUS_SHAPES].join(", ");
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: "unknownShape",
|
|
40
|
+
data: {
|
|
41
|
+
shape: trimmed,
|
|
42
|
+
valid: validList + (suggestion ? `. Did you mean '${suggestion}'?` : "")
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function findClosestShape(input) {
|
|
47
|
+
for (const shape of RADIUS_SHAPES) if (shape.startsWith(input.slice(0, 3)) || input.startsWith(shape.slice(0, 3))) return shape;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ImportDeclaration(node) {
|
|
52
|
+
ctx.trackImport(node);
|
|
53
|
+
},
|
|
54
|
+
"CallExpression ObjectExpression"(node) {
|
|
55
|
+
if (!ctx.isStyleObject(node)) return;
|
|
56
|
+
for (const prop of node.properties) {
|
|
57
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
58
|
+
if (getKeyName(prop.key) !== "radius") continue;
|
|
59
|
+
const str = getStringValue(prop.value);
|
|
60
|
+
if (str) {
|
|
61
|
+
checkRadiusValue(str, prop.value);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
65
|
+
if (stateProp.type !== "Property") continue;
|
|
66
|
+
const stateStr = getStringValue(stateProp.value);
|
|
67
|
+
if (stateStr) checkRadiusValue(stateStr, stateProp.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { valid_radius_shape_default as default };
|
|
77
|
+
//# sourceMappingURL=valid-radius-shape.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-radius-shape.mjs","names":[],"sources":["../../src/rules/valid-radius-shape.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { RADIUS_SHAPES } from '../constants.js';\n\ntype MessageIds = 'unknownShape';\n\nconst SHAPE_LIKE = /^[a-z]+$/;\n\nexport default createRule<[], MessageIds>({\n name: 'valid-radius-shape',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate special shape keywords used with the radius property',\n },\n messages: {\n unknownShape:\n \"Unknown radius shape '{{shape}}'. Valid shapes: {{valid}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkRadiusValue(value: string, node: TSESTree.Node): void {\n const trimmed = value.trim();\n // Only check single-word values that look like keywords\n if (!SHAPE_LIKE.test(trimmed)) return;\n\n // Known valid keywords\n if (RADIUS_SHAPES.has(trimmed)) return;\n if (trimmed === 'true' || trimmed === 'false') return;\n if (trimmed === 'none' || trimmed === 'inherit' || trimmed === 'initial')\n return;\n\n // Check if it's a directional modifier (handled elsewhere)\n const directions = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n ]);\n if (directions.has(trimmed)) return;\n\n // It looks like they tried to use a shape keyword\n const suggestion = findClosestShape(trimmed);\n const validList = [...RADIUS_SHAPES].join(', ');\n\n context.report({\n node,\n messageId: 'unknownShape',\n data: {\n shape: trimmed,\n valid:\n validList + (suggestion ? `. Did you mean '${suggestion}'?` : ''),\n },\n });\n }\n\n function findClosestShape(input: string): string | null {\n for (const shape of RADIUS_SHAPES) {\n if (\n shape.startsWith(input.slice(0, 3)) ||\n input.startsWith(shape.slice(0, 3))\n ) {\n return shape;\n }\n }\n return null;\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key !== 'radius') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkRadiusValue(str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkRadiusValue(stateStr, stateProp.value);\n }\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,aAAa;AAEnB,iCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iEACH;EACD,UAAU,EACR,cACE,8DACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,iBAAiB,OAAe,MAA2B;GAClE,MAAM,UAAU,MAAM,MAAM;AAE5B,OAAI,CAAC,WAAW,KAAK,QAAQ,CAAE;AAG/B,OAAI,cAAc,IAAI,QAAQ,CAAE;AAChC,OAAI,YAAY,UAAU,YAAY,QAAS;AAC/C,OAAI,YAAY,UAAU,YAAY,aAAa,YAAY,UAC7D;AAaF,OAVmB,IAAI,IAAI;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACD,CAAC,CACa,IAAI,QAAQ,CAAE;GAG7B,MAAM,aAAa,iBAAiB,QAAQ;GAC5C,MAAM,YAAY,CAAC,GAAG,cAAc,CAAC,KAAK,KAAK;AAE/C,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KACJ,OAAO;KACP,OACE,aAAa,aAAa,mBAAmB,WAAW,MAAM;KACjE;IACF,CAAC;;EAGJ,SAAS,iBAAiB,OAA8B;AACtD,QAAK,MAAM,SAAS,cAClB,KACE,MAAM,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC,IACnC,MAAM,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC,CAEnC,QAAO;AAGX,UAAO;;AAGT,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,SADY,WAAW,KAAK,IAAI,KACpB,SAAU;KAEtB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,KAAK;AACP,uBAAiB,KAAK,KAAK,MAAM;AACjC;;AAGF,SAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,UAAI,UAAU,SAAS,WAAY;MACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,UAAI,SACF,kBAAiB,UAAU,UAAU,MAAM;;;;GAMtD;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { TastyContext } from "../context.mjs";
|
|
3
|
+
import { getKeyName, getStringValue } from "../utils.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/rules/valid-recipe.ts
|
|
6
|
+
var valid_recipe_default = createRule({
|
|
7
|
+
name: "valid-recipe",
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: { description: "Validate recipe property values against config" },
|
|
11
|
+
messages: { unknownRecipe: "Unknown recipe '{{name}}'." },
|
|
12
|
+
schema: []
|
|
13
|
+
},
|
|
14
|
+
defaultOptions: [],
|
|
15
|
+
create(context) {
|
|
16
|
+
const ctx = new TastyContext(context);
|
|
17
|
+
function checkRecipeValue(value, node) {
|
|
18
|
+
if (ctx.config.recipes.length === 0) return;
|
|
19
|
+
const sections = value.split("|");
|
|
20
|
+
for (const section of sections) {
|
|
21
|
+
const names = section.trim().split(/\s+/);
|
|
22
|
+
for (const name of names) {
|
|
23
|
+
if (name.length === 0) continue;
|
|
24
|
+
if (!ctx.config.recipes.includes(name)) context.report({
|
|
25
|
+
node,
|
|
26
|
+
messageId: "unknownRecipe",
|
|
27
|
+
data: { name }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
ImportDeclaration(node) {
|
|
34
|
+
ctx.trackImport(node);
|
|
35
|
+
},
|
|
36
|
+
"CallExpression ObjectExpression"(node) {
|
|
37
|
+
if (!ctx.isStyleObject(node)) return;
|
|
38
|
+
for (const prop of node.properties) {
|
|
39
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
40
|
+
if (getKeyName(prop.key) !== "recipe") continue;
|
|
41
|
+
const str = getStringValue(prop.value);
|
|
42
|
+
if (str) checkRecipeValue(str, prop.value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { valid_recipe_default as default };
|
|
51
|
+
//# sourceMappingURL=valid-recipe.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-recipe.mjs","names":[],"sources":["../../src/rules/valid-recipe.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds = 'unknownRecipe';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-recipe',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate recipe property values against config',\n },\n messages: {\n unknownRecipe: \"Unknown recipe '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkRecipeValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.recipes.length === 0) return;\n\n // Split by | for pre/post merge separation\n const sections = value.split('|');\n for (const section of sections) {\n const names = section.trim().split(/\\s+/);\n for (const name of names) {\n if (name.length === 0) continue;\n if (!ctx.config.recipes.includes(name)) {\n context.report({\n node,\n messageId: 'unknownRecipe',\n data: { name },\n });\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key !== 'recipe') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkRecipeValue(str, prop.value);\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAOA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,kDACd;EACD,UAAU,EACR,eAAe,8BAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,iBAAiB,OAAe,MAA2B;AAClE,OAAI,IAAI,OAAO,QAAQ,WAAW,EAAG;GAGrC,MAAM,WAAW,MAAM,MAAM,IAAI;AACjC,QAAK,MAAM,WAAW,UAAU;IAC9B,MAAM,QAAQ,QAAQ,MAAM,CAAC,MAAM,MAAM;AACzC,SAAK,MAAM,QAAQ,OAAO;AACxB,SAAI,KAAK,WAAW,EAAG;AACvB,SAAI,CAAC,IAAI,OAAO,QAAQ,SAAS,KAAK,CACpC,SAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM;MACf,CAAC;;;;AAMV,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,SADY,WAAW,KAAK,IAAI,KACpB,SAAU;KAEtB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,IACF,kBAAiB,KAAK,KAAK,MAAM;;;GAIxC;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { TastyContext } from "../context.mjs";
|
|
3
|
+
import { getKeyName, getStringValue, validateStateKey } from "../utils.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/rules/valid-state-key.ts
|
|
6
|
+
var valid_state_key_default = createRule({
|
|
7
|
+
name: "valid-state-key",
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: { description: "Validate state key syntax in style mapping objects" },
|
|
11
|
+
messages: {
|
|
12
|
+
invalidStateKey: "{{reason}}",
|
|
13
|
+
ownOutsideSubElement: "@own() can only be used inside sub-element styles."
|
|
14
|
+
},
|
|
15
|
+
schema: []
|
|
16
|
+
},
|
|
17
|
+
defaultOptions: [],
|
|
18
|
+
create(context) {
|
|
19
|
+
const ctx = new TastyContext(context);
|
|
20
|
+
function isInsideSubElement(node) {
|
|
21
|
+
let current = node.parent;
|
|
22
|
+
while (current) {
|
|
23
|
+
if (current.type === "Property" && !current.computed && current.key.type === "Identifier" && /^[A-Z]/.test(current.key.name)) return true;
|
|
24
|
+
current = current.parent;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
function checkStateMapKeys(obj, insideSubElement) {
|
|
29
|
+
for (const prop of obj.properties) {
|
|
30
|
+
if (prop.type !== "Property") continue;
|
|
31
|
+
const key = !prop.computed ? getKeyName(prop.key) : getStringValue(prop.key);
|
|
32
|
+
if (key === null) continue;
|
|
33
|
+
const error = validateStateKey(key);
|
|
34
|
+
if (error) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: prop.key,
|
|
37
|
+
messageId: "invalidStateKey",
|
|
38
|
+
data: { reason: error }
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (key.includes("@own(") && !insideSubElement) context.report({
|
|
43
|
+
node: prop.key,
|
|
44
|
+
messageId: "ownOutsideSubElement"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
ImportDeclaration(node) {
|
|
50
|
+
ctx.trackImport(node);
|
|
51
|
+
},
|
|
52
|
+
"CallExpression ObjectExpression"(node) {
|
|
53
|
+
if (!ctx.isStyleObject(node)) return;
|
|
54
|
+
const insideSubElement = isInsideSubElement(node);
|
|
55
|
+
for (const prop of node.properties) {
|
|
56
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
57
|
+
const key = getKeyName(prop.key);
|
|
58
|
+
if (key === null) continue;
|
|
59
|
+
if (/^[A-Z]/.test(key) || key.startsWith("@") || key.startsWith("&")) continue;
|
|
60
|
+
if (prop.value.type === "ObjectExpression") {
|
|
61
|
+
if (!/^[A-Z]/.test(key)) checkStateMapKeys(prop.value, insideSubElement);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { valid_state_key_default as default };
|
|
71
|
+
//# sourceMappingURL=valid-state-key.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-state-key.mjs","names":[],"sources":["../../src/rules/valid-state-key.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue, validateStateKey } from '../utils.js';\n\ntype MessageIds = 'invalidStateKey' | 'ownOutsideSubElement';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-state-key',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate state key syntax in style mapping objects',\n },\n messages: {\n invalidStateKey: '{{reason}}',\n ownOutsideSubElement:\n '@own() can only be used inside sub-element styles.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInsideSubElement(node: TSESTree.Node): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === 'Property' &&\n !current.computed &&\n current.key.type === 'Identifier' &&\n /^[A-Z]/.test(current.key.name)\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkStateMapKeys(\n obj: TSESTree.ObjectExpression,\n insideSubElement: boolean,\n ): void {\n for (const prop of obj.properties) {\n if (prop.type !== 'Property') continue;\n\n const key = !prop.computed\n ? getKeyName(prop.key)\n : getStringValue(prop.key);\n if (key === null) continue;\n\n // Validate syntax\n const error = validateStateKey(key);\n if (error) {\n context.report({\n node: prop.key,\n messageId: 'invalidStateKey',\n data: { reason: error },\n });\n continue;\n }\n\n // Check @own() usage\n if (key.includes('@own(') && !insideSubElement) {\n context.report({\n node: prop.key,\n messageId: 'ownOutsideSubElement',\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const insideSubElement = isInsideSubElement(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n // Skip non-style-property keys\n if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n // If value is an object, check state map keys\n if (prop.value.type === 'ObjectExpression') {\n // Determine if this is actually a sub-element\n const isSubEl = /^[A-Z]/.test(key);\n if (!isSubEl) {\n checkStateMapKeys(prop.value, insideSubElement);\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,sDACd;EACD,UAAU;GACR,iBAAiB;GACjB,sBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,mBAAmB,MAA8B;GACxD,IAAI,UAAqC,KAAK;AAC9C,UAAO,SAAS;AACd,QACE,QAAQ,SAAS,cACjB,CAAC,QAAQ,YACT,QAAQ,IAAI,SAAS,gBACrB,SAAS,KAAK,QAAQ,IAAI,KAAK,CAE/B,QAAO;AAET,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,kBACP,KACA,kBACM;AACN,QAAK,MAAM,QAAQ,IAAI,YAAY;AACjC,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MAAM,CAAC,KAAK,WACd,WAAW,KAAK,IAAI,GACpB,eAAe,KAAK,IAAI;AAC5B,QAAI,QAAQ,KAAM;IAGlB,MAAM,QAAQ,iBAAiB,IAAI;AACnC,QAAI,OAAO;AACT,aAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,MAAM,EAAE,QAAQ,OAAO;MACxB,CAAC;AACF;;AAIF,QAAI,IAAI,SAAS,QAAQ,IAAI,CAAC,iBAC5B,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACZ,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;IAE9B,MAAM,mBAAmB,mBAAmB,KAAK;AAEjD,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;KAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,SAAI,QAAQ,KAAM;AAGlB,SAAI,SAAS,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,CAClE;AAGF,SAAI,KAAK,MAAM,SAAS,oBAGtB;UAAI,CADY,SAAS,KAAK,IAAI,CAEhC,mBAAkB,KAAK,OAAO,iBAAiB;;;;GAKxD;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { TastyContext } from "../context.mjs";
|
|
3
|
+
import { getKeyName, getStringValue } from "../utils.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/rules/valid-styles-structure.ts
|
|
6
|
+
var valid_styles_structure_default = createRule({
|
|
7
|
+
name: "valid-styles-structure",
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: { description: "Validate overall structure of styles object passed to tasty APIs" },
|
|
11
|
+
messages: {
|
|
12
|
+
stateKeyAtTopLevel: "State key '{{key}}' at top level is not valid. State maps belong inside property values, not at the root of the styles object.",
|
|
13
|
+
invalidKeyframesStructure: "@keyframes value must be an object of { name: { step: styles } }.",
|
|
14
|
+
invalidPropertiesStructure: "@properties value must be an object of { name: { syntax, inherits, initialValue } }.",
|
|
15
|
+
recipeNotString: "'recipe' value must be a string."
|
|
16
|
+
},
|
|
17
|
+
schema: []
|
|
18
|
+
},
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
create(context) {
|
|
21
|
+
const ctx = new TastyContext(context);
|
|
22
|
+
const STATE_KEY_PATTERNS = [
|
|
23
|
+
/^:/,
|
|
24
|
+
/^\./,
|
|
25
|
+
/^\[/
|
|
26
|
+
];
|
|
27
|
+
function looksLikeStateKey(key) {
|
|
28
|
+
if (key === "") return true;
|
|
29
|
+
return STATE_KEY_PATTERNS.some((p) => p.test(key));
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
ImportDeclaration(node) {
|
|
33
|
+
ctx.trackImport(node);
|
|
34
|
+
},
|
|
35
|
+
"CallExpression ObjectExpression"(node) {
|
|
36
|
+
if (!ctx.isStyleObject(node)) return;
|
|
37
|
+
for (const prop of node.properties) {
|
|
38
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
39
|
+
const key = getKeyName(prop.key);
|
|
40
|
+
if (key === null) continue;
|
|
41
|
+
if (looksLikeStateKey(key)) {
|
|
42
|
+
context.report({
|
|
43
|
+
node: prop.key,
|
|
44
|
+
messageId: "stateKeyAtTopLevel",
|
|
45
|
+
data: { key }
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (key === "@keyframes") {
|
|
50
|
+
if (prop.value.type !== "ObjectExpression") context.report({
|
|
51
|
+
node: prop.value,
|
|
52
|
+
messageId: "invalidKeyframesStructure"
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (key === "@properties") {
|
|
57
|
+
if (prop.value.type !== "ObjectExpression") context.report({
|
|
58
|
+
node: prop.value,
|
|
59
|
+
messageId: "invalidPropertiesStructure"
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (key === "recipe") {
|
|
64
|
+
if (getStringValue(prop.value) === null && prop.value.type !== "Literal") {
|
|
65
|
+
if (prop.value.type !== "TemplateLiteral" || prop.value.expressions.length > 0) context.report({
|
|
66
|
+
node: prop.value,
|
|
67
|
+
messageId: "recipeNotString"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { valid_styles_structure_default as default };
|
|
79
|
+
//# sourceMappingURL=valid-styles-structure.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-styles-structure.mjs","names":[],"sources":["../../src/rules/valid-styles-structure.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds =\n | 'stateKeyAtTopLevel'\n | 'invalidKeyframesStructure'\n | 'invalidPropertiesStructure'\n | 'recipeNotString';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-styles-structure',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate overall structure of styles object passed to tasty APIs',\n },\n messages: {\n stateKeyAtTopLevel:\n \"State key '{{key}}' at top level is not valid. State maps belong inside property values, not at the root of the styles object.\",\n invalidKeyframesStructure:\n '@keyframes value must be an object of { name: { step: styles } }.',\n invalidPropertiesStructure:\n '@properties value must be an object of { name: { syntax, inherits, initialValue } }.',\n recipeNotString: \"'recipe' value must be a string.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n const STATE_KEY_PATTERNS = [\n /^:/, // pseudo-class\n /^\\./, // class selector\n /^\\[/, // attribute selector\n ];\n\n function looksLikeStateKey(key: string): boolean {\n if (key === '') return true;\n return STATE_KEY_PATTERNS.some((p) => p.test(key));\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n // Check for state keys at top level (common mistake)\n if (looksLikeStateKey(key)) {\n context.report({\n node: prop.key,\n messageId: 'stateKeyAtTopLevel',\n data: { key },\n });\n continue;\n }\n\n // Validate @keyframes structure\n if (key === '@keyframes') {\n if (prop.value.type !== 'ObjectExpression') {\n context.report({\n node: prop.value,\n messageId: 'invalidKeyframesStructure',\n });\n }\n continue;\n }\n\n // Validate @properties structure\n if (key === '@properties') {\n if (prop.value.type !== 'ObjectExpression') {\n context.report({\n node: prop.value,\n messageId: 'invalidPropertiesStructure',\n });\n }\n continue;\n }\n\n // Validate recipe is a string\n if (key === 'recipe') {\n const str = getStringValue(prop.value);\n if (str === null && prop.value.type !== 'Literal') {\n // Allow string literals, template literals without expressions\n if (\n prop.value.type !== 'TemplateLiteral' ||\n prop.value.expressions.length > 0\n ) {\n context.report({\n node: prop.value,\n messageId: 'recipeNotString',\n });\n }\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAWA,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,oEACH;EACD,UAAU;GACR,oBACE;GACF,2BACE;GACF,4BACE;GACF,iBAAiB;GAClB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,MAAM,qBAAqB;GACzB;GACA;GACA;GACD;EAED,SAAS,kBAAkB,KAAsB;AAC/C,OAAI,QAAQ,GAAI,QAAO;AACvB,UAAO,mBAAmB,MAAM,MAAM,EAAE,KAAK,IAAI,CAAC;;AAGpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;KAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,SAAI,QAAQ,KAAM;AAGlB,SAAI,kBAAkB,IAAI,EAAE;AAC1B,cAAQ,OAAO;OACb,MAAM,KAAK;OACX,WAAW;OACX,MAAM,EAAE,KAAK;OACd,CAAC;AACF;;AAIF,SAAI,QAAQ,cAAc;AACxB,UAAI,KAAK,MAAM,SAAS,mBACtB,SAAQ,OAAO;OACb,MAAM,KAAK;OACX,WAAW;OACZ,CAAC;AAEJ;;AAIF,SAAI,QAAQ,eAAe;AACzB,UAAI,KAAK,MAAM,SAAS,mBACtB,SAAQ,OAAO;OACb,MAAM,KAAK;OACX,WAAW;OACZ,CAAC;AAEJ;;AAIF,SAAI,QAAQ,UAEV;UADY,eAAe,KAAK,MAAM,KAC1B,QAAQ,KAAK,MAAM,SAAS,WAEtC;WACE,KAAK,MAAM,SAAS,qBACpB,KAAK,MAAM,YAAY,SAAS,EAEhC,SAAQ,OAAO;QACb,MAAM,KAAK;QACX,WAAW;QACZ,CAAC;;;;;GAMb;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { TastyContext } from "../context.mjs";
|
|
3
|
+
import { getKeyName } from "../utils.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/rules/valid-sub-element.ts
|
|
6
|
+
var valid_sub_element_default = createRule({
|
|
7
|
+
name: "valid-sub-element",
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: { description: "Validate sub-element key format in style objects" },
|
|
11
|
+
messages: { subElementNotObject: "Sub-element '{{name}}' value must be a style object, not a {{type}}." },
|
|
12
|
+
schema: []
|
|
13
|
+
},
|
|
14
|
+
defaultOptions: [],
|
|
15
|
+
create(context) {
|
|
16
|
+
const ctx = new TastyContext(context);
|
|
17
|
+
return {
|
|
18
|
+
ImportDeclaration(node) {
|
|
19
|
+
ctx.trackImport(node);
|
|
20
|
+
},
|
|
21
|
+
"CallExpression ObjectExpression"(node) {
|
|
22
|
+
if (!ctx.isStyleObject(node)) return;
|
|
23
|
+
for (const prop of node.properties) {
|
|
24
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
25
|
+
const key = getKeyName(prop.key);
|
|
26
|
+
if (key === null || !/^[A-Z]/.test(key)) continue;
|
|
27
|
+
if (prop.value.type !== "ObjectExpression") {
|
|
28
|
+
const valueType = prop.value.type === "Literal" ? typeof prop.value.value : prop.value.type;
|
|
29
|
+
context.report({
|
|
30
|
+
node: prop.value,
|
|
31
|
+
messageId: "subElementNotObject",
|
|
32
|
+
data: {
|
|
33
|
+
name: key,
|
|
34
|
+
type: valueType
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { valid_sub_element_default as default };
|
|
46
|
+
//# sourceMappingURL=valid-sub-element.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-sub-element.mjs","names":[],"sources":["../../src/rules/valid-sub-element.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName } from '../utils.js';\n\ntype MessageIds = 'subElementNotObject';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-sub-element',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate sub-element key format in style objects',\n },\n messages: {\n subElementNotObject:\n \"Sub-element '{{name}}' value must be a style object, not a {{type}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null || !/^[A-Z]/.test(key)) continue;\n\n if (prop.value.type !== 'ObjectExpression') {\n const valueType =\n prop.value.type === 'Literal'\n ? typeof prop.value.value\n : prop.value.type;\n\n context.report({\n node: prop.value,\n messageId: 'subElementNotObject',\n data: { name: key, type: valueType },\n });\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAOA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,oDACd;EACD,UAAU,EACR,qBACE,wEACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;AAErC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;KAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,SAAI,QAAQ,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAE;AAEzC,SAAI,KAAK,MAAM,SAAS,oBAAoB;MAC1C,MAAM,YACJ,KAAK,MAAM,SAAS,YAChB,OAAO,KAAK,MAAM,QAClB,KAAK,MAAM;AAEjB,cAAQ,OAAO;OACb,MAAM,KAAK;OACX,WAAW;OACX,MAAM;QAAE,MAAM;QAAK,MAAM;QAAW;OACrC,CAAC;;;;GAIT;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { KNOWN_CSS_PROPERTIES, SEMANTIC_TRANSITIONS } from "../constants.mjs";
|
|
3
|
+
import { TastyContext } from "../context.mjs";
|
|
4
|
+
import { getKeyName, getStringValue } from "../utils.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/rules/valid-transition.ts
|
|
7
|
+
var valid_transition_default = createRule({
|
|
8
|
+
name: "valid-transition",
|
|
9
|
+
meta: {
|
|
10
|
+
type: "suggestion",
|
|
11
|
+
docs: { description: "Validate transition property values use valid semantic transition names" },
|
|
12
|
+
messages: { unknownTransition: "Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name." },
|
|
13
|
+
schema: []
|
|
14
|
+
},
|
|
15
|
+
defaultOptions: [],
|
|
16
|
+
create(context) {
|
|
17
|
+
const ctx = new TastyContext(context);
|
|
18
|
+
function checkTransitionValue(value, node) {
|
|
19
|
+
const groups = value.split(",");
|
|
20
|
+
for (const group of groups) {
|
|
21
|
+
const parts = group.trim().split(/\s+/);
|
|
22
|
+
if (parts.length === 0) continue;
|
|
23
|
+
const name = parts[0];
|
|
24
|
+
if (name.startsWith("$$")) continue;
|
|
25
|
+
if (!SEMANTIC_TRANSITIONS.has(name) && !KNOWN_CSS_PROPERTIES.has(name) && name !== "all" && name !== "none") context.report({
|
|
26
|
+
node,
|
|
27
|
+
messageId: "unknownTransition",
|
|
28
|
+
data: {
|
|
29
|
+
name,
|
|
30
|
+
known: [...SEMANTIC_TRANSITIONS].join(", ")
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
ImportDeclaration(node) {
|
|
37
|
+
ctx.trackImport(node);
|
|
38
|
+
},
|
|
39
|
+
"CallExpression ObjectExpression"(node) {
|
|
40
|
+
if (!ctx.isStyleObject(node)) return;
|
|
41
|
+
for (const prop of node.properties) {
|
|
42
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
43
|
+
if (getKeyName(prop.key) !== "transition") continue;
|
|
44
|
+
const str = getStringValue(prop.value);
|
|
45
|
+
if (str) {
|
|
46
|
+
checkTransitionValue(str, prop.value);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
50
|
+
if (stateProp.type !== "Property") continue;
|
|
51
|
+
const stateStr = getStringValue(stateProp.value);
|
|
52
|
+
if (stateStr) checkTransitionValue(stateStr, stateProp.value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { valid_transition_default as default };
|
|
62
|
+
//# sourceMappingURL=valid-transition.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-transition.mjs","names":[],"sources":["../../src/rules/valid-transition.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { SEMANTIC_TRANSITIONS, KNOWN_CSS_PROPERTIES } from '../constants.js';\n\ntype MessageIds = 'unknownTransition';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-transition',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Validate transition property values use valid semantic transition names',\n },\n messages: {\n unknownTransition:\n \"Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n const groups = value.split(',');\n\n for (const group of groups) {\n const parts = group.trim().split(/\\s+/);\n if (parts.length === 0) continue;\n\n const name = parts[0];\n\n // $$ prefix is always valid (custom property reference)\n if (name.startsWith('$$')) continue;\n\n if (\n !SEMANTIC_TRANSITIONS.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(name) &&\n name !== 'all' &&\n name !== 'none'\n ) {\n context.report({\n node,\n messageId: 'unknownTransition',\n data: {\n name,\n known: [...SEMANTIC_TRANSITIONS].join(', '),\n },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkTransitionValue(stateStr, stateProp.value);\n }\n }\n }\n }\n },\n };\n },\n});\n"],"mappings":";;;;;;AAQA,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,2EACH;EACD,UAAU,EACR,mBACE,+FACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,qBAAqB,OAAe,MAA2B;GACtE,MAAM,SAAS,MAAM,MAAM,IAAI;AAE/B,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM;AACvC,QAAI,MAAM,WAAW,EAAG;IAExB,MAAM,OAAO,MAAM;AAGnB,QAAI,KAAK,WAAW,KAAK,CAAE;AAE3B,QACE,CAAC,qBAAqB,IAAI,KAAK,IAC/B,CAAC,qBAAqB,IAAI,KAAK,IAC/B,SAAS,SACT,SAAS,OAET,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ;MACA,OAAO,CAAC,GAAG,qBAAqB,CAAC,KAAK,KAAK;MAC5C;KACF,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,SADY,WAAW,KAAK,IAAI,KACpB,aAAc;KAE1B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,KAAK;AACP,2BAAqB,KAAK,KAAK,MAAM;AACrC;;AAGF,SAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,UAAI,UAAU,SAAS,WAAY;MACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,UAAI,SACF,sBAAqB,UAAU,UAAU,MAAM;;;;GAM1D;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.mjs";
|
|
2
|
+
import { TastyContext } from "../context.mjs";
|
|
3
|
+
import { getKeyName, getStringValue } from "../utils.mjs";
|
|
4
|
+
import { getParser } from "../parser.mjs";
|
|
5
|
+
import { getExpectation } from "../property-expectations.mjs";
|
|
6
|
+
|
|
7
|
+
//#region src/rules/valid-value.ts
|
|
8
|
+
var valid_value_default = createRule({
|
|
9
|
+
name: "valid-value",
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: { description: "Parse style values through the tasty parser and validate against per-property expectations" },
|
|
13
|
+
messages: {
|
|
14
|
+
unbalancedParens: "Unbalanced parentheses in value.",
|
|
15
|
+
importantNotAllowed: "Do not use !important in tasty styles. Use state specificity instead.",
|
|
16
|
+
unexpectedMod: "Unrecognized token '{{mod}}' in '{{property}}' value. This may be a typo.",
|
|
17
|
+
unexpectedColor: "Property '{{property}}' does not accept color tokens, but found '{{color}}'.",
|
|
18
|
+
invalidMod: "Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}."
|
|
19
|
+
},
|
|
20
|
+
schema: []
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
const ctx = new TastyContext(context);
|
|
25
|
+
function checkParenBalance(value, node) {
|
|
26
|
+
let depth = 0;
|
|
27
|
+
for (const char of value) {
|
|
28
|
+
if (char === "(") depth++;
|
|
29
|
+
if (char === ")") depth--;
|
|
30
|
+
if (depth < 0) {
|
|
31
|
+
context.report({
|
|
32
|
+
node,
|
|
33
|
+
messageId: "unbalancedParens"
|
|
34
|
+
});
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (depth !== 0) {
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
messageId: "unbalancedParens"
|
|
42
|
+
});
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function checkValue(value, property, node) {
|
|
48
|
+
if (!checkParenBalance(value, node)) return;
|
|
49
|
+
if (value.includes("!important")) {
|
|
50
|
+
context.report({
|
|
51
|
+
node,
|
|
52
|
+
messageId: "importantNotAllowed"
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!property) return;
|
|
57
|
+
const result = getParser(ctx.config).process(value);
|
|
58
|
+
const expectation = getExpectation(property);
|
|
59
|
+
for (const group of result.groups) {
|
|
60
|
+
if (!expectation.acceptsColor && group.colors.length > 0) for (const color of group.colors) context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: "unexpectedColor",
|
|
63
|
+
data: {
|
|
64
|
+
property,
|
|
65
|
+
color
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (expectation.acceptsMods === false && group.mods.length > 0) for (const mod of group.mods) context.report({
|
|
69
|
+
node,
|
|
70
|
+
messageId: "unexpectedMod",
|
|
71
|
+
data: {
|
|
72
|
+
property,
|
|
73
|
+
mod
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
else if (Array.isArray(expectation.acceptsMods) && group.mods.length > 0) {
|
|
77
|
+
const allowed = new Set(expectation.acceptsMods);
|
|
78
|
+
for (const mod of group.mods) if (!allowed.has(mod)) context.report({
|
|
79
|
+
node,
|
|
80
|
+
messageId: "invalidMod",
|
|
81
|
+
data: {
|
|
82
|
+
property,
|
|
83
|
+
mod,
|
|
84
|
+
accepted: expectation.acceptsMods.join(", ")
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function processProperty(prop) {
|
|
91
|
+
const key = !prop.computed ? getKeyName(prop.key) : null;
|
|
92
|
+
if (key && (/^[A-Z]/.test(key) || key.startsWith("@"))) return;
|
|
93
|
+
if (key && (key.startsWith("$") || key.startsWith("#"))) return;
|
|
94
|
+
if (key && key.startsWith("&")) return;
|
|
95
|
+
const str = getStringValue(prop.value);
|
|
96
|
+
if (str) {
|
|
97
|
+
checkValue(str, key, prop.value);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
101
|
+
if (stateProp.type !== "Property") continue;
|
|
102
|
+
const stateStr = getStringValue(stateProp.value);
|
|
103
|
+
if (stateStr) checkValue(stateStr, key, stateProp.value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
ImportDeclaration(node) {
|
|
108
|
+
ctx.trackImport(node);
|
|
109
|
+
},
|
|
110
|
+
"CallExpression ObjectExpression"(node) {
|
|
111
|
+
if (!ctx.isStyleObject(node)) return;
|
|
112
|
+
for (const prop of node.properties) {
|
|
113
|
+
if (prop.type !== "Property") continue;
|
|
114
|
+
processProperty(prop);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { valid_value_default as default };
|
|
123
|
+
//# sourceMappingURL=valid-value.mjs.map
|