@tenphi/eslint-plugin-tasty 1.9.0 → 1.10.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/README.md CHANGED
@@ -227,7 +227,7 @@ not matched.
227
227
  | `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
228
228
  | `tasty/no-runtime-styles-mutation` | warn | Render-time JavaScript in Tasty style values (props, state, hook results, parameters, local bindings, calls, computed keys); use states and tokens instead. Module-scope constants, imports, `undefined` and static `mergeStyles(...)` calls are fixed at load and allowed |
229
229
  | `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; recommends `mergeStyles` for multiple root spreads; intentional spreads need a per-line suppression and reason |
230
- | `tasty/no-style-prop` | warn | `style` on recognized Tasty components; use token references and the `tokens` prop ([details](docs/rules/no-style-prop.md)) |
230
+ | `tasty/no-style-prop` | warn | `style` on recognized Tasty components; use token references and the `tokens` prop. The `elements` option covers every component or element, so it can replace React's `style` restriction ([details](docs/rules/no-style-prop.md)) |
231
231
  | `tasty/no-classname-prop` | warn | `className` on recognized Tasty components; keep styling in Tasty or use `data-element` for sub-elements ([details](docs/rules/no-classname-prop.md)) |
232
232
  | `tasty/no-styles-prop` | warn | Instance `styles` on recognized Tasty components; use `tokens`, `mods`, exposed props/variants, or `tasty(Component, { styles })` ([details](docs/rules/no-styles-prop.md)) |
233
233
  | `tasty/consistent-token-usage` | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers |
@@ -384,6 +384,12 @@ attribute or spread property. JSX child comments can use
384
384
  `{/* eslint-disable-next-line tasty/no-classname-prop -- library requires this class */}`
385
385
  immediately above the affected element when the attribute is on that next line.
386
386
 
387
+ If `react/forbid-component-props` or `react/forbid-dom-props` also forbids
388
+ `style`, a `style` exception has to name both rules, and the React part hides
389
+ every other prop that rule forbids on the line. Set `tasty/no-style-prop`'s
390
+ `elements` option and remove `style` from the React rules, so one precise rule
391
+ handles it; see [replacing React's `style` restriction](docs/rules/no-style-prop.md#replacing-reacts-style-restriction).
392
+
387
393
  Suppression uses ESLint's normal directives. The plugin does not enforce comment
388
394
  reasons or infer exceptions automatically. A warning severity alone does not mean
389
395
  the value is valid: checks for unknown names, malformed syntax, state order, and
@@ -1,22 +1,46 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { TastyContext } from "../context.js";
3
- import { tastyComponentPropListeners } from "./tasty-component-props.js";
3
+ import { jsxPropListeners } from "./tasty-component-props.js";
4
4
 
5
5
  //#region src/rules/no-style-prop.ts
6
+ const KINDS = {
7
+ tasty: new Set(["tasty"]),
8
+ components: new Set(["tasty", "component"]),
9
+ all: new Set([
10
+ "tasty",
11
+ "component",
12
+ "element"
13
+ ])
14
+ };
6
15
  var no_style_prop_default = createRule({
7
16
  name: "no-style-prop",
8
17
  meta: {
9
18
  type: "suggestion",
10
19
  docs: { description: "Prefer tokens over React inline style on Tasty components" },
11
- messages: { noStyleProp: "Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason." },
12
- schema: []
20
+ messages: {
21
+ noStyleProp: "Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.",
22
+ noInlineStyle: "Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason."
23
+ },
24
+ schema: [{
25
+ type: "object",
26
+ properties: { elements: {
27
+ type: "string",
28
+ enum: [
29
+ "tasty",
30
+ "components",
31
+ "all"
32
+ ]
33
+ } },
34
+ additionalProperties: false
35
+ }]
13
36
  },
14
- defaultOptions: [],
15
- create(context) {
16
- return tastyComponentPropListeners(new TastyContext(context), (name, node) => {
17
- if (name === "style") context.report({
37
+ defaultOptions: [{ elements: "tasty" }],
38
+ create(context, [{ elements = "tasty" }]) {
39
+ return jsxPropListeners(new TastyContext(context), KINDS[elements], (name, node, kind) => {
40
+ if (name !== "style") return;
41
+ context.report({
18
42
  node,
19
- messageId: "noStyleProp"
43
+ messageId: kind === "tasty" ? "noStyleProp" : "noInlineStyle"
20
44
  });
21
45
  });
22
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"no-style-prop.js","names":[],"sources":["../../src/rules/no-style-prop.ts"],"sourcesContent":["import { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { tastyComponentPropListeners } from './tasty-component-props.js';\n\nexport default createRule<[], 'noStyleProp'>({\n name: 'no-style-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Prefer tokens over React inline style on Tasty components',\n },\n messages: {\n noStyleProp:\n \"Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n return tastyComponentPropListeners(ctx, (name, node) => {\n if (name === 'style') context.report({ node, messageId: 'noStyleProp' });\n });\n },\n});\n"],"mappings":";;;;;AAIA,4BAAe,WAA8B;CAC3C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6DACd;EACD,UAAU,EACR,aACE,0SACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;AAEd,SAAO,4BADK,IAAI,aAAa,QAAQ,GACI,MAAM,SAAS;AACtD,OAAI,SAAS,QAAS,SAAQ,OAAO;IAAE;IAAM,WAAW;IAAe,CAAC;IACxE;;CAEL,CAAC"}
1
+ {"version":3,"file":"no-style-prop.js","names":[],"sources":["../../src/rules/no-style-prop.ts"],"sourcesContent":["import { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport {\n type JSXElementKind,\n jsxPropListeners,\n} from './tasty-component-props.js';\n\ntype Elements = 'tasty' | 'components' | 'all';\n\nconst KINDS: Record<Elements, ReadonlySet<JSXElementKind>> = {\n tasty: new Set(['tasty']),\n components: new Set(['tasty', 'component']),\n all: new Set(['tasty', 'component', 'element']),\n};\n\nexport default createRule<\n [{ elements?: Elements }],\n 'noStyleProp' | 'noInlineStyle'\n>({\n name: 'no-style-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Prefer tokens over React inline style on Tasty components',\n },\n messages: {\n noStyleProp:\n \"Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n noInlineStyle:\n \"Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n },\n schema: [\n {\n type: 'object',\n properties: {\n elements: { type: 'string', enum: ['tasty', 'components', 'all'] },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ elements: 'tasty' }],\n create(context, [{ elements = 'tasty' }]) {\n const ctx = new TastyContext(context);\n return jsxPropListeners(ctx, KINDS[elements], (name, node, kind) => {\n if (name !== 'style') return;\n context.report({\n node,\n messageId: kind === 'tasty' ? 'noStyleProp' : 'noInlineStyle',\n });\n });\n },\n});\n"],"mappings":";;;;;AASA,MAAM,QAAuD;CAC3D,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC;CACzB,YAAY,IAAI,IAAI,CAAC,SAAS,YAAY,CAAC;CAC3C,KAAK,IAAI,IAAI;EAAC;EAAS;EAAa;EAAU,CAAC;CAChD;AAED,4BAAe,WAGb;CACA,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6DACd;EACD,UAAU;GACR,aACE;GACF,eACE;GACH;EACD,QAAQ,CACN;GACE,MAAM;GACN,YAAY,EACV,UAAU;IAAE,MAAM;IAAU,MAAM;KAAC;KAAS;KAAc;KAAM;IAAE,EACnE;GACD,sBAAsB;GACvB,CACF;EACF;CACD,gBAAgB,CAAC,EAAE,UAAU,SAAS,CAAC;CACvC,OAAO,SAAS,CAAC,EAAE,WAAW,YAAY;AAExC,SAAO,iBADK,IAAI,aAAa,QAAQ,EACR,MAAM,YAAY,MAAM,MAAM,SAAS;AAClE,OAAI,SAAS,QAAS;AACtB,WAAQ,OAAO;IACb;IACA,WAAW,SAAS,UAAU,gBAAgB;IAC/C,CAAC;IACF;;CAEL,CAAC"}
@@ -1,33 +1,46 @@
1
1
  import { getKeyName, unwrapExpression } from "../utils.js";
2
2
 
3
3
  //#region src/rules/tasty-component-props.ts
4
- /** Inspect explicit props and inline object spreads without evaluating spread bindings. */
5
- function tastyComponentPropListeners(ctx, check) {
6
- function visitSpread(node) {
4
+ /**
5
+ * Inspect explicit props and inline object spreads on every JSX element without
6
+ * evaluating spread bindings. `kinds` limits which elements are classified and
7
+ * checked, so a Tasty-only rule never runs component recognition elsewhere.
8
+ */
9
+ function jsxPropListeners(ctx, kinds, check) {
10
+ function visitSpread(node, kind) {
7
11
  node = unwrapExpression(node);
8
12
  if (node.type !== "ObjectExpression") return;
9
- for (const prop of node.properties) if (prop.type === "SpreadElement") visitSpread(prop.argument);
13
+ for (const prop of node.properties) if (prop.type === "SpreadElement") visitSpread(prop.argument, kind);
10
14
  else {
11
15
  const name = prop.computed ? getKeyName(unwrapExpression(prop.key)) : getKeyName(prop.key);
12
- if (name !== null && (!prop.computed || unwrapExpression(prop.key).type === "Literal")) check(name, prop);
16
+ if (name !== null && (!prop.computed || unwrapExpression(prop.key).type === "Literal")) check(name, prop, kind);
13
17
  }
14
18
  }
19
+ function classify(node) {
20
+ let name = node.name;
21
+ while (name.type === "JSXMemberExpression") name = name.object;
22
+ if (name.type !== "JSXIdentifier" || node.name.type === "JSXIdentifier" && !/^[A-Z]/.test(name.name)) return kinds.has("element") ? "element" : null;
23
+ if (kinds.has("tasty") && ctx.isTastyJSXComponent(node, name.name)) return "tasty";
24
+ return kinds.has("component") ? "component" : null;
25
+ }
15
26
  return {
16
27
  ImportDeclaration(node) {
17
28
  ctx.trackImport(node);
18
29
  },
19
30
  JSXOpeningElement(node) {
20
- let name = node.name;
21
- while (name.type === "JSXMemberExpression") name = name.object;
22
- if (name.type !== "JSXIdentifier") return;
23
- if (node.name.type === "JSXIdentifier" && !/^[A-Z]/.test(name.name)) return;
24
- if (!ctx.isTastyJSXComponent(node, name.name)) return;
25
- for (const attribute of node.attributes) if (attribute.type === "JSXSpreadAttribute") visitSpread(attribute.argument);
26
- else if (attribute.name.type === "JSXIdentifier") check(attribute.name.name, attribute);
31
+ const kind = classify(node);
32
+ if (!kind) return;
33
+ for (const attribute of node.attributes) if (attribute.type === "JSXSpreadAttribute") visitSpread(attribute.argument, kind);
34
+ else if (attribute.name.type === "JSXIdentifier") check(attribute.name.name, attribute, kind);
27
35
  }
28
36
  };
29
37
  }
38
+ const TASTY_ONLY = new Set(["tasty"]);
39
+ /** Inspect explicit props and inline object spreads on recognized Tasty components. */
40
+ function tastyComponentPropListeners(ctx, check) {
41
+ return jsxPropListeners(ctx, TASTY_ONLY, (name, node) => check(name, node));
42
+ }
30
43
 
31
44
  //#endregion
32
- export { tastyComponentPropListeners };
45
+ export { jsxPropListeners, tastyComponentPropListeners };
33
46
  //# sourceMappingURL=tasty-component-props.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tasty-component-props.js","names":[],"sources":["../../src/rules/tasty-component-props.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { TastyContext } from '../context.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\n\n/** Inspect explicit props and inline object spreads without evaluating spread bindings. */\nexport function tastyComponentPropListeners(\n ctx: TastyContext,\n check: (name: string, node: TSESTree.Node) => void,\n) {\n function visitSpread(node: TSESTree.Node): void {\n node = unwrapExpression(node);\n if (node.type !== 'ObjectExpression') return;\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n visitSpread(prop.argument);\n } else {\n const name = prop.computed\n ? getKeyName(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // A computed identifier is dynamic; a computed string literal is visible.\n if (\n name !== null &&\n (!prop.computed || unwrapExpression(prop.key).type === 'Literal')\n ) {\n check(name, prop);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n ctx.trackImport(node);\n },\n JSXOpeningElement(node: TSESTree.JSXOpeningElement) {\n let name = node.name;\n while (name.type === 'JSXMemberExpression') name = name.object;\n if (name.type !== 'JSXIdentifier') return;\n if (node.name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name))\n return;\n if (!ctx.isTastyJSXComponent(node, name.name)) return;\n for (const attribute of node.attributes) {\n if (attribute.type === 'JSXSpreadAttribute')\n visitSpread(attribute.argument);\n else if (attribute.name.type === 'JSXIdentifier')\n check(attribute.name.name, attribute);\n }\n },\n };\n}\n"],"mappings":";;;;AAKA,SAAgB,4BACd,KACA,OACA;CACA,SAAS,YAAY,MAA2B;AAC9C,SAAO,iBAAiB,KAAK;AAC7B,MAAI,KAAK,SAAS,mBAAoB;AACtC,OAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,gBAChB,aAAY,KAAK,SAAS;OACrB;GACL,MAAM,OAAO,KAAK,WACd,WAAW,iBAAiB,KAAK,IAAI,CAAC,GACtC,WAAW,KAAK,IAAI;AAExB,OACE,SAAS,SACR,CAAC,KAAK,YAAY,iBAAiB,KAAK,IAAI,CAAC,SAAS,WAEvD,OAAM,MAAM,KAAK;;;AAMzB,QAAO;EACL,kBAAkB,MAAkC;AAClD,OAAI,YAAY,KAAK;;EAEvB,kBAAkB,MAAkC;GAClD,IAAI,OAAO,KAAK;AAChB,UAAO,KAAK,SAAS,sBAAuB,QAAO,KAAK;AACxD,OAAI,KAAK,SAAS,gBAAiB;AACnC,OAAI,KAAK,KAAK,SAAS,mBAAmB,CAAC,SAAS,KAAK,KAAK,KAAK,CACjE;AACF,OAAI,CAAC,IAAI,oBAAoB,MAAM,KAAK,KAAK,CAAE;AAC/C,QAAK,MAAM,aAAa,KAAK,WAC3B,KAAI,UAAU,SAAS,qBACrB,aAAY,UAAU,SAAS;YACxB,UAAU,KAAK,SAAS,gBAC/B,OAAM,UAAU,KAAK,MAAM,UAAU;;EAG5C"}
1
+ {"version":3,"file":"tasty-component-props.js","names":[],"sources":["../../src/rules/tasty-component-props.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { TastyContext } from '../context.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\n\n/**\n * What a JSX element is, for prop rules: a recognized Tasty component, any\n * other component (capitalized or member tag), or a native element.\n */\nexport type JSXElementKind = 'tasty' | 'component' | 'element';\n\n/**\n * Inspect explicit props and inline object spreads on every JSX element without\n * evaluating spread bindings. `kinds` limits which elements are classified and\n * checked, so a Tasty-only rule never runs component recognition elsewhere.\n */\nexport function jsxPropListeners(\n ctx: TastyContext,\n kinds: ReadonlySet<JSXElementKind>,\n check: (name: string, node: TSESTree.Node, kind: JSXElementKind) => void,\n) {\n function visitSpread(node: TSESTree.Node, kind: JSXElementKind): void {\n node = unwrapExpression(node);\n if (node.type !== 'ObjectExpression') return;\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n visitSpread(prop.argument, kind);\n } else {\n const name = prop.computed\n ? getKeyName(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // A computed identifier is dynamic; a computed string literal is visible.\n if (\n name !== null &&\n (!prop.computed || unwrapExpression(prop.key).type === 'Literal')\n ) {\n check(name, prop, kind);\n }\n }\n }\n }\n\n function classify(node: TSESTree.JSXOpeningElement): JSXElementKind | null {\n let name = node.name;\n while (name.type === 'JSXMemberExpression') name = name.object;\n // `<svg:rect>` and lowercase tags are native; `<motion.div>` is a component.\n if (\n name.type !== 'JSXIdentifier' ||\n (node.name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name))\n ) {\n return kinds.has('element') ? 'element' : null;\n }\n if (kinds.has('tasty') && ctx.isTastyJSXComponent(node, name.name))\n return 'tasty';\n return kinds.has('component') ? 'component' : null;\n }\n\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n ctx.trackImport(node);\n },\n JSXOpeningElement(node: TSESTree.JSXOpeningElement) {\n const kind = classify(node);\n if (!kind) return;\n for (const attribute of node.attributes) {\n if (attribute.type === 'JSXSpreadAttribute')\n visitSpread(attribute.argument, kind);\n else if (attribute.name.type === 'JSXIdentifier')\n check(attribute.name.name, attribute, kind);\n }\n },\n };\n}\n\nconst TASTY_ONLY = new Set<JSXElementKind>(['tasty']);\n\n/** Inspect explicit props and inline object spreads on recognized Tasty components. */\nexport function tastyComponentPropListeners(\n ctx: TastyContext,\n check: (name: string, node: TSESTree.Node) => void,\n) {\n return jsxPropListeners(ctx, TASTY_ONLY, (name, node) => check(name, node));\n}\n"],"mappings":";;;;;;;;AAeA,SAAgB,iBACd,KACA,OACA,OACA;CACA,SAAS,YAAY,MAAqB,MAA4B;AACpE,SAAO,iBAAiB,KAAK;AAC7B,MAAI,KAAK,SAAS,mBAAoB;AACtC,OAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,gBAChB,aAAY,KAAK,UAAU,KAAK;OAC3B;GACL,MAAM,OAAO,KAAK,WACd,WAAW,iBAAiB,KAAK,IAAI,CAAC,GACtC,WAAW,KAAK,IAAI;AAExB,OACE,SAAS,SACR,CAAC,KAAK,YAAY,iBAAiB,KAAK,IAAI,CAAC,SAAS,WAEvD,OAAM,MAAM,MAAM,KAAK;;;CAM/B,SAAS,SAAS,MAAyD;EACzE,IAAI,OAAO,KAAK;AAChB,SAAO,KAAK,SAAS,sBAAuB,QAAO,KAAK;AAExD,MACE,KAAK,SAAS,mBACb,KAAK,KAAK,SAAS,mBAAmB,CAAC,SAAS,KAAK,KAAK,KAAK,CAEhE,QAAO,MAAM,IAAI,UAAU,GAAG,YAAY;AAE5C,MAAI,MAAM,IAAI,QAAQ,IAAI,IAAI,oBAAoB,MAAM,KAAK,KAAK,CAChE,QAAO;AACT,SAAO,MAAM,IAAI,YAAY,GAAG,cAAc;;AAGhD,QAAO;EACL,kBAAkB,MAAkC;AAClD,OAAI,YAAY,KAAK;;EAEvB,kBAAkB,MAAkC;GAClD,MAAM,OAAO,SAAS,KAAK;AAC3B,OAAI,CAAC,KAAM;AACX,QAAK,MAAM,aAAa,KAAK,WAC3B,KAAI,UAAU,SAAS,qBACrB,aAAY,UAAU,UAAU,KAAK;YAC9B,UAAU,KAAK,SAAS,gBAC/B,OAAM,UAAU,KAAK,MAAM,WAAW,KAAK;;EAGlD;;AAGH,MAAM,aAAa,IAAI,IAAoB,CAAC,QAAQ,CAAC;;AAGrD,SAAgB,4BACd,KACA,OACA;AACA,QAAO,iBAAiB,KAAK,aAAa,MAAM,SAAS,MAAM,MAAM,KAAK,CAAC"}
@@ -4,16 +4,16 @@ import { TastyContext, styleObjectListeners } from "../context.js";
4
4
  import selectorParser from "postcss-selector-parser";
5
5
 
6
6
  //#region src/rules/valid-sub-element.ts
7
- /** Expressions whose result is visibly not an object. */
8
- const NON_OBJECT_EXPRESSIONS = new Set([
9
- "TemplateLiteral",
10
- "ArrayExpression",
11
- "ArrowFunctionExpression",
12
- "FunctionExpression",
13
- "ClassExpression",
14
- "UnaryExpression",
15
- "BinaryExpression",
16
- "UpdateExpression"
7
+ /** Expressions whose result is visibly not an object, as named in a report. */
8
+ const NON_OBJECT_EXPRESSIONS = new Map([
9
+ ["TemplateLiteral", "a string"],
10
+ ["ArrayExpression", "an array"],
11
+ ["ArrowFunctionExpression", "a function"],
12
+ ["FunctionExpression", "a function"],
13
+ ["ClassExpression", "a class"],
14
+ ["UnaryExpression", "a primitive value"],
15
+ ["BinaryExpression", "a primitive value"],
16
+ ["UpdateExpression", "a number"]
17
17
  ]);
18
18
  const LEGACY_PSEUDO_ELEMENTS = new Set([
19
19
  ":before",
@@ -67,7 +67,7 @@ var valid_sub_element_default = createRule({
67
67
  type: "problem",
68
68
  docs: { description: "Validate sub-element values and prevent root-targeting affixes" },
69
69
  messages: {
70
- subElementNotObject: "Sub-element '{{name}}' value must be a style object, not a {{type}}.",
70
+ subElementNotObject: "Sub-element '{{name}}' value must be a style object, not {{type}}.",
71
71
  subElementTargetsRoot: "Sub-element '{{name}}' selector '{{selector}}' styles the containing element instead of a sub-element. Use property state maps in that containing scope, or a descendant '$' selector such as 'h1'. Pseudo-elements such as '&::before' are allowed."
72
72
  },
73
73
  schema: []
@@ -95,14 +95,21 @@ var valid_sub_element_default = createRule({
95
95
  }
96
96
  }
97
97
  /**
98
- * The type to name in the report when a value can never be a style object,
99
- * or `null` when it is `false` (disables the sub-element) or might be one.
98
+ * What to name in the report when a value can never be a style object, or
99
+ * `null` when it might be one or is `false` or `null`. Both disable the
100
+ * sub-element, and in a later `mergeStyles` layer or a `styles` prop they
101
+ * remove the one below; `undefined` (`void 0`) leaves it as it is.
100
102
  * A `const` binding is followed to what it names; any other binding, member
101
103
  * or call is opaque, and Tasty accepts a reference to a style object there.
102
104
  */
103
105
  function nonObjectType(node, seen) {
104
106
  node = unwrapExpression(node);
105
- if (node.type === "Literal") return node.value === false ? null : typeof node.value;
107
+ if (node.type === "Literal") {
108
+ if ("regex" in node && node.regex) return "a regular expression";
109
+ if (node.value === false || node.value === null) return null;
110
+ return `a ${typeof node.value}`;
111
+ }
112
+ if (node.type === "UnaryExpression" && node.operator === "void") return null;
106
113
  if (node.type === "Identifier") {
107
114
  if (seen.has(node)) return null;
108
115
  seen.add(node);
@@ -114,7 +121,7 @@ var valid_sub_element_default = createRule({
114
121
  const init = unwrapExpression(def.node.init);
115
122
  return init.type === "ObjectExpression" ? null : nonObjectType(init, seen);
116
123
  }
117
- return NON_OBJECT_EXPRESSIONS.has(node.type) ? node.type : null;
124
+ return NON_OBJECT_EXPRESSIONS.get(node.type) ?? null;
118
125
  }
119
126
  function checkStyleObject(node) {
120
127
  if (visited.has(node)) return;
@@ -1 +1 @@
1
- {"version":3,"file":"valid-sub-element.js","names":[],"sources":["../../src/rules/valid-sub-element.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'subElementNotObject' | 'subElementTargetsRoot';\n\n/** Expressions whose result is visibly not an object. */\nconst NON_OBJECT_EXPRESSIONS = new Set<string>([\n 'TemplateLiteral',\n 'ArrayExpression',\n 'ArrowFunctionExpression',\n 'FunctionExpression',\n 'ClassExpression',\n 'UnaryExpression',\n 'BinaryExpression',\n 'UpdateExpression',\n]);\n\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nfunction hasDescendantTarget(nodes: selectorParser.Node[]): boolean {\n let lastCharacter = '';\n for (const node of nodes.slice(1)) {\n if (node.type === 'combinator') {\n if (node.value.trim()) return true;\n // The affix renderer skips authored whitespace. Tags insert their own\n // separator; pseudos attach directly, and classes/attributes attach\n // after a tag, class, attribute or placeholder.\n continue;\n }\n if (node.type === 'tag' || node.type === 'universal') {\n if (lastCharacter) return true;\n } else if (node.type === 'class' || node.type === 'attribute') {\n if (lastCharacter && !/[\\]@a-zA-Z0-9-]/.test(lastCharacter)) return true;\n }\n lastCharacter = node.toString().trim().slice(-1);\n }\n return false;\n}\n\nfunction firstRootSelector(selector: string, key: string): string | null {\n try {\n const root = selectorParser().astSync(selector);\n for (const branch of root.nodes) {\n const nodes = branch.nodes.filter((node) => node.type !== 'comment');\n if (nodes[0]?.type !== 'nesting' || nodes.length < 2) continue;\n // An explicit placeholder creates a descendant selector even with an\n // ampersand prefix. A different trailing uppercase name injects the key\n // as its descendant; the sub-element's own name does not.\n if (\n nodes.some((node) => node.type === 'tag' && node.value.startsWith('@'))\n )\n continue;\n const trailingElement = branch\n .toString()\n .trim()\n .slice(1)\n .trim()\n .match(/(?:^|[\\s>+~\\]:])([A-Z][a-zA-Z0-9]*)$/);\n if (trailingElement && trailingElement[1] !== key) continue;\n // Follow Tasty's affix normalization, rather than CSS whitespace alone.\n // Nested :is() arguments do not change the outer selector's target.\n if (hasDescendantTarget(nodes)) continue;\n const hasPseudoElement = nodes.some((node) => {\n if (node.type !== 'pseudo') return false;\n let name = node.value.toLowerCase();\n if (name.startsWith('::')) return true;\n if (name.includes('\\\\')) {\n const identifier = selectorParser().astSync(name.slice(1)).first\n ?.first;\n if (identifier?.type === 'tag')\n name = ':' + identifier.value.toLowerCase();\n }\n return LEGACY_PSEUDO_ELEMENTS.has(name);\n });\n if (!hasPseudoElement) return branch.toString().trim();\n }\n } catch {\n // Invalid/incomplete selector text belongs to syntax validation.\n }\n return null;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-sub-element',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate sub-element values and prevent root-targeting affixes',\n },\n messages: {\n subElementNotObject:\n \"Sub-element '{{name}}' value must be a style object, not a {{type}}.\",\n subElementTargetsRoot:\n \"Sub-element '{{name}}' selector '{{selector}}' styles the containing element instead of a sub-element. Use property state maps in that containing scope, or a descendant '$' selector such as 'h1'. Pseudo-elements such as '&::before' are allowed.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function checkAffix(name: string, node: TSESTree.ObjectExpression) {\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key !== '$') continue;\n const selector = getStringValue(unwrapExpression(prop.value));\n if (selector === null) continue;\n const rootSelector = firstRootSelector(selector, name);\n if (rootSelector === null) continue;\n context.report({\n node: prop.value,\n messageId: 'subElementTargetsRoot',\n data: { name, selector: rootSelector },\n });\n }\n }\n\n /**\n * The type to name in the report when a value can never be a style object,\n * or `null` when it is `false` (disables the sub-element) or might be one.\n * A `const` binding is followed to what it names; any other binding, member\n * or call is opaque, and Tasty accepts a reference to a style object there.\n */\n function nonObjectType(\n node: TSESTree.Node,\n seen: Set<TSESTree.Node>,\n ): string | null {\n node = unwrapExpression(node);\n if (node.type === 'Literal') {\n return node.value === false ? null : typeof node.value;\n }\n if (node.type === 'Identifier') {\n if (seen.has(node)) return null;\n seen.add(node);\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope && !scope.set.has(node.name)) scope = scope.upper;\n const variable = scope?.set.get(node.name);\n const def = variable?.defs.length === 1 ? variable.defs[0] : undefined;\n if (\n def?.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n !def.node.init\n ) {\n return null;\n }\n const init = unwrapExpression(def.node.init);\n return init.type === 'ObjectExpression'\n ? null\n : nonObjectType(init, seen);\n }\n return NON_OBJECT_EXPRESSIONS.has(node.type) ? node.type : null;\n }\n\n // Recurse only through owned sub-elements. Some shared/JSX listeners match\n // just the root, and descriptor objects and state maps are not styles.\n function checkStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node)) return;\n visited.add(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key === null || !/^[A-Z]/.test(key)) continue;\n\n const value = unwrapExpression(prop.value);\n if (value.type === 'ObjectExpression') {\n checkAffix(key, value);\n checkStyleObject(value);\n } else {\n const valueType = nonObjectType(value, new Set());\n if (valueType === null) continue;\n\n context.report({\n node: prop.value,\n messageId: 'subElementNotObject',\n data: { name: key, type: valueType },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (ctx.isStyleObject(node)) checkStyleObject(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"valid-sub-element.js","names":[],"sources":["../../src/rules/valid-sub-element.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'subElementNotObject' | 'subElementTargetsRoot';\n\n/** Expressions whose result is visibly not an object, as named in a report. */\nconst NON_OBJECT_EXPRESSIONS = new Map<string, string>([\n ['TemplateLiteral', 'a string'],\n ['ArrayExpression', 'an array'],\n ['ArrowFunctionExpression', 'a function'],\n ['FunctionExpression', 'a function'],\n ['ClassExpression', 'a class'],\n ['UnaryExpression', 'a primitive value'],\n ['BinaryExpression', 'a primitive value'],\n ['UpdateExpression', 'a number'],\n]);\n\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nfunction hasDescendantTarget(nodes: selectorParser.Node[]): boolean {\n let lastCharacter = '';\n for (const node of nodes.slice(1)) {\n if (node.type === 'combinator') {\n if (node.value.trim()) return true;\n // The affix renderer skips authored whitespace. Tags insert their own\n // separator; pseudos attach directly, and classes/attributes attach\n // after a tag, class, attribute or placeholder.\n continue;\n }\n if (node.type === 'tag' || node.type === 'universal') {\n if (lastCharacter) return true;\n } else if (node.type === 'class' || node.type === 'attribute') {\n if (lastCharacter && !/[\\]@a-zA-Z0-9-]/.test(lastCharacter)) return true;\n }\n lastCharacter = node.toString().trim().slice(-1);\n }\n return false;\n}\n\nfunction firstRootSelector(selector: string, key: string): string | null {\n try {\n const root = selectorParser().astSync(selector);\n for (const branch of root.nodes) {\n const nodes = branch.nodes.filter((node) => node.type !== 'comment');\n if (nodes[0]?.type !== 'nesting' || nodes.length < 2) continue;\n // An explicit placeholder creates a descendant selector even with an\n // ampersand prefix. A different trailing uppercase name injects the key\n // as its descendant; the sub-element's own name does not.\n if (\n nodes.some((node) => node.type === 'tag' && node.value.startsWith('@'))\n )\n continue;\n const trailingElement = branch\n .toString()\n .trim()\n .slice(1)\n .trim()\n .match(/(?:^|[\\s>+~\\]:])([A-Z][a-zA-Z0-9]*)$/);\n if (trailingElement && trailingElement[1] !== key) continue;\n // Follow Tasty's affix normalization, rather than CSS whitespace alone.\n // Nested :is() arguments do not change the outer selector's target.\n if (hasDescendantTarget(nodes)) continue;\n const hasPseudoElement = nodes.some((node) => {\n if (node.type !== 'pseudo') return false;\n let name = node.value.toLowerCase();\n if (name.startsWith('::')) return true;\n if (name.includes('\\\\')) {\n const identifier = selectorParser().astSync(name.slice(1)).first\n ?.first;\n if (identifier?.type === 'tag')\n name = ':' + identifier.value.toLowerCase();\n }\n return LEGACY_PSEUDO_ELEMENTS.has(name);\n });\n if (!hasPseudoElement) return branch.toString().trim();\n }\n } catch {\n // Invalid/incomplete selector text belongs to syntax validation.\n }\n return null;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-sub-element',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate sub-element values and prevent root-targeting affixes',\n },\n messages: {\n subElementNotObject:\n \"Sub-element '{{name}}' value must be a style object, not {{type}}.\",\n subElementTargetsRoot:\n \"Sub-element '{{name}}' selector '{{selector}}' styles the containing element instead of a sub-element. Use property state maps in that containing scope, or a descendant '$' selector such as 'h1'. Pseudo-elements such as '&::before' are allowed.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function checkAffix(name: string, node: TSESTree.ObjectExpression) {\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key !== '$') continue;\n const selector = getStringValue(unwrapExpression(prop.value));\n if (selector === null) continue;\n const rootSelector = firstRootSelector(selector, name);\n if (rootSelector === null) continue;\n context.report({\n node: prop.value,\n messageId: 'subElementTargetsRoot',\n data: { name, selector: rootSelector },\n });\n }\n }\n\n /**\n * What to name in the report when a value can never be a style object, or\n * `null` when it might be one or is `false` or `null`. Both disable the\n * sub-element, and in a later `mergeStyles` layer or a `styles` prop they\n * remove the one below; `undefined` (`void 0`) leaves it as it is.\n * A `const` binding is followed to what it names; any other binding, member\n * or call is opaque, and Tasty accepts a reference to a style object there.\n */\n function nonObjectType(\n node: TSESTree.Node,\n seen: Set<TSESTree.Node>,\n ): string | null {\n node = unwrapExpression(node);\n if (node.type === 'Literal') {\n if ('regex' in node && node.regex) return 'a regular expression';\n if (node.value === false || node.value === null) return null;\n return `a ${typeof node.value}`;\n }\n if (node.type === 'UnaryExpression' && node.operator === 'void') {\n return null;\n }\n if (node.type === 'Identifier') {\n if (seen.has(node)) return null;\n seen.add(node);\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope && !scope.set.has(node.name)) scope = scope.upper;\n const variable = scope?.set.get(node.name);\n const def = variable?.defs.length === 1 ? variable.defs[0] : undefined;\n if (\n def?.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n !def.node.init\n ) {\n return null;\n }\n const init = unwrapExpression(def.node.init);\n return init.type === 'ObjectExpression'\n ? null\n : nonObjectType(init, seen);\n }\n return NON_OBJECT_EXPRESSIONS.get(node.type) ?? null;\n }\n\n // Recurse only through owned sub-elements. Some shared/JSX listeners match\n // just the root, and descriptor objects and state maps are not styles.\n function checkStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node)) return;\n visited.add(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key === null || !/^[A-Z]/.test(key)) continue;\n\n const value = unwrapExpression(prop.value);\n if (value.type === 'ObjectExpression') {\n checkAffix(key, value);\n checkStyleObject(value);\n } else {\n const valueType = nonObjectType(value, new Set());\n if (valueType === null) continue;\n\n context.report({\n node: prop.value,\n messageId: 'subElementNotObject',\n data: { name: key, type: valueType },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (ctx.isStyleObject(node)) checkStyleObject(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {