@tenphi/eslint-plugin-tasty 1.0.4 → 1.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.
Files changed (36) hide show
  1. package/README.md +149 -4
  2. package/dist/config.js +12 -3
  3. package/dist/config.js.map +1 -1
  4. package/dist/configs.js +1 -0
  5. package/dist/configs.js.map +1 -1
  6. package/dist/constants.js +294 -38
  7. package/dist/constants.js.map +1 -1
  8. package/dist/context.js +154 -26
  9. package/dist/context.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/property-expectations.js +71 -2
  14. package/dist/property-expectations.js.map +1 -1
  15. package/dist/rules/consistent-token-usage.js +12 -1
  16. package/dist/rules/consistent-token-usage.js.map +1 -1
  17. package/dist/rules/no-own-at-root.js +1 -9
  18. package/dist/rules/no-own-at-root.js.map +1 -1
  19. package/dist/rules/no-raw-transition-duration.js +141 -0
  20. package/dist/rules/no-raw-transition-duration.js.map +1 -0
  21. package/dist/rules/no-unknown-state-alias.js +2 -2
  22. package/dist/rules/no-unknown-state-alias.js.map +1 -1
  23. package/dist/rules/prefer-shorthand-property.js +32 -13
  24. package/dist/rules/prefer-shorthand-property.js.map +1 -1
  25. package/dist/rules/valid-boolean-property.js +19 -8
  26. package/dist/rules/valid-boolean-property.js.map +1 -1
  27. package/dist/rules/valid-directional-modifier.js +16 -1
  28. package/dist/rules/valid-directional-modifier.js.map +1 -1
  29. package/dist/rules/valid-state-key.js +4 -13
  30. package/dist/rules/valid-state-key.js.map +1 -1
  31. package/dist/rules/valid-sub-element.js +5 -4
  32. package/dist/rules/valid-sub-element.js.map +1 -1
  33. package/dist/types.d.ts +33 -1
  34. package/dist/utils.js +8 -21
  35. package/dist/utils.js.map +1 -1
  36. package/package.json +5 -3
package/dist/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_IMPORT_SOURCES, KNOWN_CSS_PROPERTIES, KNOWN_TASTY_PROPERTIES, SPECIAL_STYLE_KEYS } from "./constants.js";
2
2
  import { loadConfig } from "./config.js";
3
- import { getKeyName } from "./utils.js";
3
+ import { getKeyName, unwrapExpression } from "./utils.js";
4
4
 
5
5
  //#region src/context.ts
6
6
  /**
@@ -90,6 +90,48 @@ function isNamedCssBlockMap(node) {
90
90
  }
91
91
  return true;
92
92
  }
93
+ /**
94
+ * The component a `styles` JSX prop overrides — `<Card styles={…} />` -> `Card`.
95
+ * A namespaced tag (`<UI.Card />`) resolves to its root object, since that is the
96
+ * name an import can be matched against.
97
+ */
98
+ function jsxStylesPropTarget(node) {
99
+ const container = node.parent;
100
+ if (container?.type !== "JSXExpressionContainer") return null;
101
+ const attribute = container.parent;
102
+ if (attribute?.type !== "JSXAttribute") return null;
103
+ const element = attribute.parent;
104
+ if (element?.type !== "JSXOpeningElement") return null;
105
+ let name = element.name;
106
+ while (name.type === "JSXMemberExpression") name = name.object;
107
+ return name.type === "JSXIdentifier" ? name.name : null;
108
+ }
109
+ /**
110
+ * The identifier a base-component argument resolves to — `tasty(UI.Card, {…})`
111
+ * -> `UI`, which is what the import map is keyed by.
112
+ *
113
+ * Type assertions are stripped on the way down. Reading `Button as any` as an
114
+ * unknown base would quietly promote an imported component to "owned", which is
115
+ * the reading that produces a warning the author cannot act on.
116
+ */
117
+ function baseComponentName(node) {
118
+ let current = unwrapExpression(node);
119
+ while (current.type === "MemberExpression") current = unwrapExpression(current.object);
120
+ return current.type === "Identifier" ? current.name : null;
121
+ }
122
+ /**
123
+ * Whether `source` is inside this project. Relative and absolute specifiers are,
124
+ * as are the conventional in-repo aliases (`~/`, `#internal`, `@/`) — `@` alone
125
+ * is not, since `@scope/pkg` is a published package.
126
+ */
127
+ function isInRepoSource(source) {
128
+ return source.startsWith(".") || source.startsWith("/") || source.startsWith("~") || source.startsWith("#") || source.startsWith("@/");
129
+ }
130
+ /** Matches a source against an `ownedSources` pattern, where `*` is any run of characters. */
131
+ function matchesSourcePattern(pattern, source) {
132
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ return new RegExp(`^${escaped.replace(/\\\*/g, ".*")}$`).test(source);
134
+ }
93
135
  const TASTY_FUNCTION_NAMES = new Set([
94
136
  "tasty",
95
137
  "tastyStatic",
@@ -105,6 +147,7 @@ const TASTY_FUNCTION_NAMES = new Set([
105
147
  var TastyContext = class {
106
148
  config;
107
149
  imports = /* @__PURE__ */ new Map();
150
+ moduleSources = /* @__PURE__ */ new Map();
108
151
  importSources;
109
152
  constructor(context, config) {
110
153
  this.context = context;
@@ -113,10 +156,11 @@ var TastyContext = class {
113
156
  }
114
157
  trackImport(node) {
115
158
  const source = node.source.value;
116
- if (!this.importSources.has(source)) return;
117
- for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier") {
159
+ for (const specifier of node.specifiers) this.moduleSources.set(specifier.local.name, source);
160
+ if (!this.importSources.has(source) || node.importKind === "type") return;
161
+ for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type") {
118
162
  const importedName = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
119
- if (TASTY_FUNCTION_NAMES.has(importedName)) this.imports.set(specifier.local.name, {
163
+ if (TASTY_FUNCTION_NAMES.has(importedName) || Object.hasOwn(this.config.styleFunctions ?? {}, importedName)) this.imports.set(specifier.local.name, {
120
164
  localName: specifier.local.name,
121
165
  importedName,
122
166
  source
@@ -126,9 +170,34 @@ var TastyContext = class {
126
170
  getImport(localName) {
127
171
  return this.imports.get(localName);
128
172
  }
173
+ /**
174
+ * Whether the base component an extension layer sits on top of lives in a file
175
+ * this project can edit — so a rule can decide whether pointing the author at
176
+ * the base's own definition is advice they can act on.
177
+ *
178
+ * A name that was never imported is declared in this very file, and an unknown
179
+ * base (`null`) counts as owned as well: staying quiet is for a base that is
180
+ * demonstrably someone else's, never for one the plugin merely failed to read.
181
+ */
182
+ isOwnedComponent(localName) {
183
+ if (localName === null) return true;
184
+ const source = this.moduleSources.get(localName);
185
+ if (source === void 0) return true;
186
+ if (isInRepoSource(source)) return true;
187
+ return (this.config.ownedSources ?? []).some((pattern) => matchesSourcePattern(pattern, source));
188
+ }
129
189
  isTastyCall(node) {
130
- if (node.callee.type !== "Identifier") return void 0;
131
- return this.imports.get(node.callee.name);
190
+ const callee = unwrapExpression(node.callee);
191
+ if (callee.type !== "Identifier") return void 0;
192
+ const imp = this.imports.get(callee.name);
193
+ if (!imp) return void 0;
194
+ let scope = this.context.sourceCode.getScope(callee);
195
+ while (scope) {
196
+ const variable = scope.set.get(callee.name);
197
+ if (variable) return variable.defs.some((def) => def.type === "ImportBinding") ? imp : void 0;
198
+ if (!scope.upper) break;
199
+ scope = scope.upper;
200
+ }
132
201
  }
133
202
  /**
134
203
  * Determines whether an object expression is a tasty style object
@@ -137,22 +206,49 @@ var TastyContext = class {
137
206
  isStyleObject(node) {
138
207
  return this.getStyleContext(node) !== null;
139
208
  }
209
+ getSubElementParent(node) {
210
+ let wrapped = node;
211
+ while (wrapped.parent && unwrapExpression(wrapped.parent) === node) wrapped = wrapped.parent;
212
+ const parent = wrapped.parent;
213
+ if (parent?.type !== "Property" || parent.computed) return null;
214
+ const key = getKeyName(parent.key);
215
+ return key && /^[A-Z]/.test(key) && parent.parent.type === "ObjectExpression" ? parent.parent : null;
216
+ }
217
+ /** Follow only sub-elements of recognized styles, never a variants container. */
218
+ getRootStyleObject(node) {
219
+ let current = node;
220
+ while (true) {
221
+ const parent = this.getSubElementParent(current);
222
+ if (!parent || !this.isStyleObject(parent)) return current;
223
+ current = parent;
224
+ }
225
+ }
226
+ /** Include helper calls nested inside a real sub-element, but not variant names. */
227
+ isInsideSubElement(node) {
228
+ let current = node.parent;
229
+ while (current) {
230
+ if (current.type === "Property" && !current.computed) {
231
+ const key = getKeyName(current.key);
232
+ if (key && /^[A-Z]/.test(key) && current.parent.type === "ObjectExpression" && this.isStyleObject(current.parent)) return true;
233
+ }
234
+ current = current.parent;
235
+ }
236
+ return false;
237
+ }
140
238
  getStyleContext(node) {
141
239
  if (node.type === "ObjectExpression") {
142
- const parent = node.parent;
143
- if (parent?.type === "Property" && !parent.computed) {
144
- const key = getKeyName(parent.key);
145
- if (key && /^[A-Z]/.test(key)) {
146
- const grandparent = parent.parent;
147
- if (grandparent?.type === "ObjectExpression") return this.getStyleContext(grandparent);
148
- }
240
+ const parent = this.getSubElementParent(node);
241
+ if (parent) {
242
+ const inherited = this.getStyleContext(parent);
243
+ if (inherited) return inherited;
149
244
  }
150
245
  }
151
246
  if (node.type === "ObjectExpression" && this.isStylesPropertyValue(node)) return {
152
247
  type: "tasty",
153
248
  isStaticCall: false,
154
249
  isSelectorMode: false,
155
- isExtending: false
250
+ isExtending: true,
251
+ baseComponent: jsxStylesPropTarget(node)
156
252
  };
157
253
  let current = node;
158
254
  while (current) {
@@ -168,7 +264,8 @@ var TastyContext = class {
168
264
  type: "useStyles",
169
265
  isStaticCall: false,
170
266
  isSelectorMode: false,
171
- isExtending: false
267
+ isExtending: false,
268
+ baseComponent: null
172
269
  };
173
270
  }
174
271
  if (name === "useGlobalStyles") {
@@ -176,16 +273,20 @@ var TastyContext = class {
176
273
  type: "useGlobalStyles",
177
274
  isStaticCall,
178
275
  isSelectorMode: true,
179
- isExtending: false
276
+ isExtending: false,
277
+ baseComponent: null
180
278
  };
181
279
  }
280
+ const custom = this.config.styleFunctions?.[name];
281
+ if (!TASTY_FUNCTION_NAMES.has(name) && custom) return this.getCustomCallContext(current, node, custom);
182
282
  return null;
183
283
  }
184
284
  if (this.isStyleVariableDeclaration(current, node)) return {
185
285
  type: "tasty",
186
286
  isStaticCall: false,
187
287
  isSelectorMode: false,
188
- isExtending: false
288
+ isExtending: false,
289
+ baseComponent: null
189
290
  };
190
291
  current = current.parent;
191
292
  }
@@ -203,6 +304,26 @@ var TastyContext = class {
203
304
  if (name === "styles" || name.endsWith("Styles")) return !(targetNode.type === "ObjectExpression" && isNamedCssBlockMap(targetNode));
204
305
  return false;
205
306
  }
307
+ getCustomCallContext(call, targetNode, config) {
308
+ const { argument, kind } = config;
309
+ if (argument !== "all" && (!Number.isInteger(argument) || argument < 0)) return null;
310
+ if (argument !== "all" && call.arguments.slice(0, argument).some((arg) => arg.type === "SpreadElement")) return null;
311
+ const args = argument === "all" ? call.arguments : [call.arguments[argument]];
312
+ for (const arg of args) {
313
+ if (!arg) continue;
314
+ const value = unwrapExpression(arg);
315
+ if (value.type !== "ObjectExpression") continue;
316
+ const isVariant = kind === "options" && this.isInsideVariantsProperty(value, targetNode);
317
+ if (kind === "styles" ? value === targetNode : kind === "options" && (this.isInsideStylesProperty(value, targetNode) || isVariant)) return {
318
+ type: "tasty",
319
+ isStaticCall: false,
320
+ isSelectorMode: false,
321
+ isExtending: !isVariant && (config.partial ?? false),
322
+ baseComponent: null
323
+ };
324
+ }
325
+ return null;
326
+ }
206
327
  /**
207
328
  * Verdict from the declared type: `true` tasty, `false` explicitly not, `null`
208
329
  * when there is no annotation to go on and the caller should fall back to the
@@ -220,18 +341,21 @@ var TastyContext = class {
220
341
  }
221
342
  getTastyCallContext(call, targetNode) {
222
343
  const args = call.arguments;
223
- const optionsArg = args.length >= 2 && args[0].type !== "ObjectExpression" ? args[1] : args[0];
344
+ const isExtending = args.length >= 2 && unwrapExpression(args[0]).type !== "ObjectExpression";
345
+ const optionsArg = isExtending ? args[1] : args[0];
224
346
  if (optionsArg?.type === "ObjectExpression" && this.isInsideStylesProperty(optionsArg, targetNode)) return {
225
347
  type: "tasty",
226
348
  isStaticCall: false,
227
349
  isSelectorMode: false,
228
- isExtending: args.length >= 2 && args[0].type !== "ObjectExpression"
350
+ isExtending,
351
+ baseComponent: isExtending ? baseComponentName(args[0]) : null
229
352
  };
230
353
  if (optionsArg?.type === "ObjectExpression" && this.isInsideVariantsProperty(optionsArg, targetNode)) return {
231
354
  type: "tasty",
232
355
  isStaticCall: false,
233
356
  isSelectorMode: false,
234
- isExtending: false
357
+ isExtending: false,
358
+ baseComponent: null
235
359
  };
236
360
  return null;
237
361
  }
@@ -241,26 +365,30 @@ var TastyContext = class {
241
365
  type: "tastyStatic",
242
366
  isStaticCall: true,
243
367
  isSelectorMode: false,
244
- isExtending: false
368
+ isExtending: false,
369
+ baseComponent: null
245
370
  };
246
371
  if (args.length === 2 && args[1] === targetNode) {
247
- const isSelectorMode = args[0].type === "Literal";
372
+ const isSelectorMode = unwrapExpression(args[0]).type === "Literal";
248
373
  return {
249
374
  type: "tastyStatic",
250
375
  isStaticCall: true,
251
376
  isSelectorMode,
252
- isExtending: !isSelectorMode
377
+ isExtending: !isSelectorMode,
378
+ baseComponent: isSelectorMode ? null : baseComponentName(args[0])
253
379
  };
254
380
  }
255
381
  return null;
256
382
  }
257
383
  isInsideStylesProperty(optionsObj, targetNode) {
258
- for (const prop of optionsObj.properties) if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "styles" && prop.value === targetNode) return true;
384
+ for (const prop of optionsObj.properties) if (prop.type === "Property" && !prop.computed && getKeyName(prop.key) === "styles" && unwrapExpression(prop.value) === targetNode) return true;
259
385
  return false;
260
386
  }
261
387
  isInsideVariantsProperty(optionsObj, targetNode) {
262
- for (const prop of optionsObj.properties) if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "variants" && prop.value.type === "ObjectExpression") {
263
- for (const variantProp of prop.value.properties) if (variantProp.type === "Property" && variantProp.value === targetNode) return true;
388
+ for (const prop of optionsObj.properties) if (prop.type === "Property" && !prop.computed && getKeyName(prop.key) === "variants") {
389
+ const variants = unwrapExpression(prop.value);
390
+ if (variants.type !== "ObjectExpression") continue;
391
+ for (const variantProp of variants.properties) if (variantProp.type === "Property" && unwrapExpression(variantProp.value) === targetNode) return true;
264
392
  }
265
393
  return false;
266
394
  }
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext } from '@typescript-eslint/utils/ts-eslint';\nimport type { ResolvedConfig } from './types.js';\nimport { loadConfig } from './config.js';\nimport {\n DEFAULT_IMPORT_SOURCES,\n KNOWN_CSS_PROPERTIES,\n KNOWN_TASTY_PROPERTIES,\n SPECIAL_STYLE_KEYS,\n} from './constants.js';\nimport { getKeyName } from './utils.js';\n\n/**\n * AST selectors that match ObjectExpressions in all known tasty style contexts:\n * call sites, variable declarations, satisfies/as expressions.\n */\nexport const STYLE_OBJECT_SELECTORS = [\n 'CallExpression ObjectExpression',\n 'VariableDeclarator > ObjectExpression',\n 'VariableDeclarator > TSSatisfiesExpression > ObjectExpression',\n 'VariableDeclarator > TSAsExpression > ObjectExpression',\n // `<Block styles={{…}} />`. Safe to match on the prop name alone: React's own\n // prop is `style`, singular, so a plural `styles` JSX prop is a Tasty\n // convention.\n \"JSXAttribute[name.name='styles'] > JSXExpressionContainer > ObjectExpression\",\n // A Storybook story's `args.styles`. Deliberately this specific rather than\n // matching any `styles` key: plenty of unrelated libraries take a `styles`\n // option object, and a bare key carries no evidence it is Tasty's. Story files\n // are where a lot of style code is authored and neither the call-site nor the\n // variable-name heuristic reaches them — the enclosing variable is named after\n // the story, and there is no Tasty call.\n // Superset of the `args.styles` shape; `isStylesPropertyValue` is the real\n // gate, so both `styles` and `'styles'` key forms are covered.\n 'Property > ObjectExpression > Property > ObjectExpression',\n] as const;\n\n/**\n * Creates a record of ESLint listeners that all point to the same handler,\n * one entry per style-object AST selector.\n */\nexport function styleObjectListeners(\n handler: (node: TSESTree.ObjectExpression) => void,\n): Record<string, (node: TSESTree.ObjectExpression) => void> {\n const listeners: Record<string, (node: TSESTree.ObjectExpression) => void> =\n {};\n\n // Selectors overlap — `tasty({ styles: {…} })` matches both\n // `CallExpression ObjectExpression` and the `styles` key selector — so the\n // handler must run at most once per object, or every rule double-reports.\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n const once = (node: TSESTree.ObjectExpression) => {\n if (visited.has(node)) return;\n visited.add(node);\n handler(node);\n };\n\n for (const selector of STYLE_OBJECT_SELECTORS) {\n listeners[selector] = once;\n }\n\n return listeners;\n}\n\nexport interface TastyImport {\n localName: string;\n importedName: string;\n source: string;\n}\n\n/**\n * Whether a type annotation names tasty's `Styles` anywhere inside it.\n *\n * Walks the annotation rather than matching only the outermost reference, so a\n * wrapped tasty type (`Styles | undefined`, `Record<string, Styles>`,\n * `{ root: Styles }`) is recognised while `CSSProperties` in the same positions is\n * not. Deliberately name-based: the plugin has no type checker, and requiring one\n * would make every rule type-aware.\n */\nfunction referencesStylesType(node: TSESTree.TypeNode): boolean {\n const stack: TSESTree.Node[] = [node];\n\n while (stack.length) {\n const current = stack.pop()!;\n\n if (\n current.type === 'TSTypeReference' &&\n current.typeName.type === 'Identifier' &&\n /^Styles$/i.test(current.typeName.name)\n ) {\n return true;\n }\n\n for (const value of Object.values(\n current as unknown as Record<string, unknown>,\n )) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item && typeof item === 'object' && 'type' in item) {\n stack.push(item as TSESTree.Node);\n }\n }\n } else if (value && typeof value === 'object' && 'type' in value) {\n // Skip `parent` — it points back up the tree and would loop forever.\n if (value !== (current as TSESTree.Node).parent) {\n stack.push(value as TSESTree.Node);\n }\n }\n }\n }\n\n return false;\n}\n\n/**\n * Whether a key could plausibly be a tasty style key.\n *\n * Covers every top-level shape tasty accepts: a style property, a sub-element\n * (capitalised), an at-rule or state/custom-property/colour key (`@`, `&`, `$`,\n * `#`), and the default-state key.\n */\nfunction isPlausibleStyleKey(key: string): boolean {\n if (key === '' || key === '_') return true;\n if (/^[A-Z]/.test(key)) return true;\n if (/^[@&$#]/.test(key)) return true;\n if (SPECIAL_STYLE_KEYS.has(key)) return true;\n\n return KNOWN_TASTY_PROPERTIES.has(key) || KNOWN_CSS_PROPERTIES.has(key);\n}\n\n/**\n * Whether an object is a map of named CSS blocks rather than a tasty styles object.\n *\n * The shape is unmistakable — `{ table: {…}, th: {…}, td: {…} }`, `{ root: {…},\n * toolbar: {…} }` — and it is what a React inline-style map looks like when it\n * carries no type annotation to opt it out. Every key is a block name that is not a\n * style key, and every value is an object.\n *\n * Requiring *every* value to be an object literal is what keeps this from swallowing\n * the case it would hurt most: `const styles = { colour: 'red' }` has a string value,\n * so it is not treated as a block map and `known-property` still reports the typo.\n * Otherwise the rule that catches a misspelled property would be silenced by the\n * misspelling.\n */\nfunction isNamedCssBlockMap(node: TSESTree.ObjectExpression): boolean {\n if (node.properties.length === 0) return false;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) return false;\n\n const key = getKeyName(prop.key);\n\n if (key === null || isPlausibleStyleKey(key)) return false;\n if (prop.value.type !== 'ObjectExpression') return false;\n }\n\n return true;\n}\n\nconst TASTY_FUNCTION_NAMES = new Set([\n 'tasty',\n 'tastyStatic',\n 'useStyles',\n 'useGlobalStyles',\n 'configure',\n]);\n\n/**\n * Context tracker for a single file's lint pass.\n * Tracks which imports come from tasty and provides\n * helpers to determine if a node is in a tasty style context.\n */\nexport class TastyContext {\n readonly config: ResolvedConfig;\n private imports = new Map<string, TastyImport>();\n private importSources: Set<string>;\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n config?: ResolvedConfig,\n ) {\n this.config = config ?? loadConfig(context.filename);\n this.importSources = new Set([\n ...DEFAULT_IMPORT_SOURCES,\n ...this.config.importSources,\n ]);\n }\n\n trackImport(node: TSESTree.ImportDeclaration): void {\n const source = node.source.value;\n if (!this.importSources.has(source)) return;\n\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportSpecifier') {\n const importedName =\n specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n if (TASTY_FUNCTION_NAMES.has(importedName)) {\n this.imports.set(specifier.local.name, {\n localName: specifier.local.name,\n importedName,\n source,\n });\n }\n }\n }\n }\n\n getImport(localName: string): TastyImport | undefined {\n return this.imports.get(localName);\n }\n\n isTastyCall(node: TSESTree.CallExpression): TastyImport | undefined {\n if (node.callee.type !== 'Identifier') return undefined;\n return this.imports.get(node.callee.name);\n }\n\n /**\n * Determines whether an object expression is a tasty style object\n * by walking up the AST to find a recognized call expression.\n */\n isStyleObject(node: TSESTree.ObjectExpression): boolean {\n return this.getStyleContext(node) !== null;\n }\n\n getStyleContext(node: TSESTree.Node): {\n type: 'tasty' | 'tastyStatic' | 'useStyles' | 'useGlobalStyles';\n isStaticCall: boolean;\n isSelectorMode: boolean;\n isExtending: boolean;\n } | null {\n // Sub-element objects inherit their parent style object's context\n if (node.type === 'ObjectExpression') {\n const parent = node.parent;\n if (parent?.type === 'Property' && !parent.computed) {\n const key = getKeyName(parent.key);\n if (key && /^[A-Z]/.test(key)) {\n const grandparent = parent.parent;\n if (grandparent?.type === 'ObjectExpression') {\n return this.getStyleContext(grandparent);\n }\n }\n }\n }\n\n // An object assigned to a `styles` key (or a `styles` JSX prop) is a style\n // object on its own terms — no enclosing tasty call or `styles`-ish variable\n // name required. Checked before the walk, which would otherwise climb past it\n // to an unrelated call or declaration and reject the whole thing.\n if (node.type === 'ObjectExpression' && this.isStylesPropertyValue(node)) {\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n let current: TSESTree.Node | undefined = node;\n\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = this.isTastyCall(current);\n if (!imp) return null;\n\n const name = imp.importedName;\n const isStaticCall = name === 'tastyStatic';\n\n if (name === 'tasty') {\n return this.getTastyCallContext(current, node);\n }\n\n if (name === 'tastyStatic') {\n return this.getTastyStaticCallContext(current, node);\n }\n\n if (name === 'useStyles') {\n if (current.arguments[0] === node) {\n return {\n type: 'useStyles',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n }\n\n if (name === 'useGlobalStyles') {\n if (current.arguments[1] === node) {\n return {\n type: 'useGlobalStyles',\n isStaticCall,\n isSelectorMode: true,\n isExtending: false,\n };\n }\n }\n\n return null;\n }\n\n if (this.isStyleVariableDeclaration(current, node)) {\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n current = current.parent;\n }\n\n return null;\n }\n\n private isStyleVariableDeclaration(\n current: TSESTree.Node,\n targetNode: TSESTree.Node,\n ): boolean {\n if (\n current.type !== 'VariableDeclarator' ||\n current.id.type !== 'Identifier'\n ) {\n return false;\n }\n\n let init: TSESTree.Node | null | undefined = current.init;\n while (\n init?.type === 'TSAsExpression' ||\n init?.type === 'TSSatisfiesExpression' ||\n init?.type === 'TSTypeAssertion' ||\n init?.type === 'TSNonNullExpression'\n ) {\n init = (init as TSESTree.TSAsExpression).expression;\n }\n\n if (init !== targetNode) return false;\n\n const name = current.id.name;\n\n if (/^[A-Z][A-Z0-9_]*$/.test(name)) return false;\n\n // An explicit type annotation is authoritative, in both directions. The name\n // heuristic below is a guess; a declared type is the author stating what the\n // object is, so it wins.\n //\n // This matters because the rewrites these rules offer are only valid *inside*\n // tasty: `#purple.05` and `$font-sans` are correct tasty declarations and\n // meaningless as raw CSS. A `const styles: Record<string, CSSProperties>`\n // handed to React's `style={…}` was matched on name alone, so its plain CSS\n // longhands were reported as tasty violations — and `--fix` rewrote real\n // `var(--shadow-sm-color)` to `#shadow-sm`, which nothing resolves there, so\n // the browser dropped the declaration.\n const annotated = this.stylesTypeAnnotation(current);\n\n if (annotated !== null) return annotated;\n\n // Only `styles` and `*Styles` count. The singular `style` is conventionally\n // a DOM inline-style object (`el.style`, `CSSProperties`, `setStyle(el, …)`)\n // holding raw CSS longhands, not tasty syntax — matching it reported those\n // longhands as tasty violations.\n if (name === 'styles' || name.endsWith('Styles')) {\n // The name is the weakest evidence there is, so let an unmistakable shape\n // overrule it. Without an annotation to go on, a map of named CSS blocks was\n // reported as tasty — `known-property` on every HTML tag name, each block read\n // as a state map — and `--fix` offered rewrites that are only valid inside\n // tasty.\n return !(\n targetNode.type === 'ObjectExpression' && isNamedCssBlockMap(targetNode)\n );\n }\n\n return false;\n }\n\n /**\n * Verdict from the declared type: `true` tasty, `false` explicitly not, `null`\n * when there is no annotation to go on and the caller should fall back to the\n * name.\n *\n * A `Styles` reference anywhere in the annotation counts, so wrappers like\n * `Styles | undefined` and `Record<string, Styles>` opt in — while\n * `CSSProperties`, `Record<string, CSSProperties>` and an inline literal of\n * them opt out.\n */\n private stylesTypeAnnotation(\n node: TSESTree.VariableDeclarator,\n ): boolean | null {\n const annotation = node.id.typeAnnotation?.typeAnnotation;\n\n if (!annotation) return null;\n\n return referencesStylesType(annotation);\n }\n\n private getTastyCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tasty({ styles: { ... } }) or tasty(Component, { styles: { ... } })\n const optionsArg =\n args.length >= 2 && args[0].type !== 'ObjectExpression'\n ? args[1]\n : args[0];\n\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideStylesProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: args.length >= 2 && args[0].type !== 'ObjectExpression',\n };\n }\n\n // Check if inside variants\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideVariantsProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n return null;\n }\n\n private getTastyStaticCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tastyStatic({ ... })\n if (args.length === 1 && args[0] === targetNode) {\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n // tastyStatic(base, { ... }) or tastyStatic('selector', { ... })\n if (args.length === 2 && args[1] === targetNode) {\n const isSelectorMode = args[0].type === 'Literal';\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode,\n isExtending: !isSelectorMode,\n };\n }\n\n return null;\n }\n\n private isInsideStylesProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 'styles' &&\n prop.value === targetNode\n ) {\n return true;\n }\n }\n return false;\n }\n\n private isInsideVariantsProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 'variants' &&\n prop.value.type === 'ObjectExpression'\n ) {\n for (const variantProp of prop.value.properties) {\n if (\n variantProp.type === 'Property' &&\n variantProp.value === targetNode\n ) {\n return true;\n }\n }\n }\n }\n return false;\n }\n\n /**\n * Whether this object is a `styles` JSX prop value or a Storybook `args.styles`.\n *\n * Both are unambiguous enough to stand on their own without an import-tracked\n * call or a `styles`-ish variable name. A bare `styles` key elsewhere is not —\n * see the selector list for why.\n */\n isStylesPropertyValue(node: TSESTree.ObjectExpression): boolean {\n const parent = node.parent;\n\n if (parent?.type === 'Property' && !parent.computed) {\n if (getKeyName(parent.key) !== 'styles') return false;\n\n // Only inside `args: { … }`.\n const argsObject = parent.parent;\n const argsProperty =\n argsObject?.type === 'ObjectExpression' ? argsObject.parent : undefined;\n\n return (\n argsProperty?.type === 'Property' &&\n !argsProperty.computed &&\n getKeyName(argsProperty.key) === 'args'\n );\n }\n\n if (parent?.type === 'JSXExpressionContainer') {\n const attribute = parent.parent;\n\n return (\n attribute?.type === 'JSXAttribute' &&\n attribute.name.type === 'JSXIdentifier' &&\n attribute.name.name === 'styles'\n );\n }\n\n return false;\n }\n\n /**\n * Checks if a property value node is a state mapping object\n * (i.e., an object where keys are state expressions and values are style values).\n */\n isStateMap(\n node: TSESTree.ObjectExpression,\n parentProperty: TSESTree.Property,\n ): boolean {\n const keyName = getKeyName(parentProperty.key);\n if (keyName === null) return false;\n\n // If the key starts with uppercase, it's a sub-element, not a state map\n if (/^[A-Z]/.test(keyName)) return false;\n\n // Special keys are not state maps\n // At-rule blocks are not state maps — their object values are descriptor maps.\n if (SPECIAL_STYLE_KEYS.has(keyName)) return false;\n\n // If the object has keys that look like state expressions, it's a state map.\n // A lone `_` (the fallback floor) or `''` (the default) is enough — both are\n // already covered by the string-literal / identifier branches below.\n return node.properties.some((prop) => {\n if (prop.type !== 'Property') return false;\n if (prop.key.type === 'Literal' && prop.key.value === '') return true;\n if (prop.key.type === 'Identifier') return true;\n if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') {\n return true;\n }\n return false;\n });\n }\n\n /**\n * Checks if a key represents a sub-element (starts with uppercase).\n */\n isSubElementKey(key: string): boolean {\n return /^[A-Z]/.test(key);\n }\n\n /**\n * Checks if a key represents a nested selector (starts with &).\n */\n isNestedSelectorKey(key: string): boolean {\n return key.startsWith('&');\n }\n\n /**\n * Checks if a key is a custom CSS property definition ($name or $$name).\n */\n isCustomPropertyKey(key: string): boolean {\n return key.startsWith('$');\n }\n\n /**\n * Checks if a key is a color token definition (#name or ##name).\n */\n isColorTokenKey(key: string): boolean {\n return key.startsWith('#');\n }\n\n /**\n * Checks if a key is a special @ property (@keyframes, @property, ...).\n */\n isSpecialKey(key: string): boolean {\n return key.startsWith('@');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext } from '@typescript-eslint/utils/ts-eslint';\nimport type { ResolvedConfig, StyleFunctionConfig } from './types.js';\nimport { loadConfig } from './config.js';\nimport {\n DEFAULT_IMPORT_SOURCES,\n KNOWN_CSS_PROPERTIES,\n KNOWN_TASTY_PROPERTIES,\n SPECIAL_STYLE_KEYS,\n} from './constants.js';\nimport { getKeyName, unwrapExpression } from './utils.js';\n\n/**\n * AST selectors that match ObjectExpressions in all known tasty style contexts:\n * call sites, variable declarations, satisfies/as expressions.\n */\nexport const STYLE_OBJECT_SELECTORS = [\n 'CallExpression ObjectExpression',\n 'VariableDeclarator > ObjectExpression',\n 'VariableDeclarator > TSSatisfiesExpression > ObjectExpression',\n 'VariableDeclarator > TSAsExpression > ObjectExpression',\n // `<Block styles={{…}} />`. Safe to match on the prop name alone: React's own\n // prop is `style`, singular, so a plural `styles` JSX prop is a Tasty\n // convention.\n \"JSXAttribute[name.name='styles'] > JSXExpressionContainer > ObjectExpression\",\n // A Storybook story's `args.styles`. Deliberately this specific rather than\n // matching any `styles` key: plenty of unrelated libraries take a `styles`\n // option object, and a bare key carries no evidence it is Tasty's. Story files\n // are where a lot of style code is authored and neither the call-site nor the\n // variable-name heuristic reaches them — the enclosing variable is named after\n // the story, and there is no Tasty call.\n // Superset of the `args.styles` shape; `isStylesPropertyValue` is the real\n // gate, so both `styles` and `'styles'` key forms are covered.\n 'Property > ObjectExpression > Property > ObjectExpression',\n] as const;\n\n/**\n * Creates a record of ESLint listeners that all point to the same handler,\n * one entry per style-object AST selector.\n */\nexport function styleObjectListeners(\n handler: (node: TSESTree.ObjectExpression) => void,\n): Record<string, (node: TSESTree.ObjectExpression) => void> {\n const listeners: Record<string, (node: TSESTree.ObjectExpression) => void> =\n {};\n\n // Selectors overlap — `tasty({ styles: {…} })` matches both\n // `CallExpression ObjectExpression` and the `styles` key selector — so the\n // handler must run at most once per object, or every rule double-reports.\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n const once = (node: TSESTree.ObjectExpression) => {\n if (visited.has(node)) return;\n visited.add(node);\n handler(node);\n };\n\n for (const selector of STYLE_OBJECT_SELECTORS) {\n listeners[selector] = once;\n }\n\n return listeners;\n}\n\nexport interface StyleContext {\n type: 'tasty' | 'tastyStatic' | 'useStyles' | 'useGlobalStyles';\n isStaticCall: boolean;\n isSelectorMode: boolean;\n isExtending: boolean;\n /**\n * Local name of the component this layer extends, when there is one and it is\n * an identifier. `null` covers every other case — a base definition, a\n * selector, or a layer whose base cannot be read off the AST — so a rule that\n * cares must treat `null` as \"unknown\", not as \"not extending\".\n */\n baseComponent: string | null;\n}\n\nexport interface TastyImport {\n localName: string;\n importedName: string;\n source: string;\n}\n\n/**\n * Whether a type annotation names tasty's `Styles` anywhere inside it.\n *\n * Walks the annotation rather than matching only the outermost reference, so a\n * wrapped tasty type (`Styles | undefined`, `Record<string, Styles>`,\n * `{ root: Styles }`) is recognised while `CSSProperties` in the same positions is\n * not. Deliberately name-based: the plugin has no type checker, and requiring one\n * would make every rule type-aware.\n */\nfunction referencesStylesType(node: TSESTree.TypeNode): boolean {\n const stack: TSESTree.Node[] = [node];\n\n while (stack.length) {\n const current = stack.pop()!;\n\n if (\n current.type === 'TSTypeReference' &&\n current.typeName.type === 'Identifier' &&\n /^Styles$/i.test(current.typeName.name)\n ) {\n return true;\n }\n\n for (const value of Object.values(\n current as unknown as Record<string, unknown>,\n )) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item && typeof item === 'object' && 'type' in item) {\n stack.push(item as TSESTree.Node);\n }\n }\n } else if (value && typeof value === 'object' && 'type' in value) {\n // Skip `parent` — it points back up the tree and would loop forever.\n if (value !== (current as TSESTree.Node).parent) {\n stack.push(value as TSESTree.Node);\n }\n }\n }\n }\n\n return false;\n}\n\n/**\n * Whether a key could plausibly be a tasty style key.\n *\n * Covers every top-level shape tasty accepts: a style property, a sub-element\n * (capitalised), an at-rule or state/custom-property/colour key (`@`, `&`, `$`,\n * `#`), and the default-state key.\n */\nfunction isPlausibleStyleKey(key: string): boolean {\n if (key === '' || key === '_') return true;\n if (/^[A-Z]/.test(key)) return true;\n if (/^[@&$#]/.test(key)) return true;\n if (SPECIAL_STYLE_KEYS.has(key)) return true;\n\n return KNOWN_TASTY_PROPERTIES.has(key) || KNOWN_CSS_PROPERTIES.has(key);\n}\n\n/**\n * Whether an object is a map of named CSS blocks rather than a tasty styles object.\n *\n * The shape is unmistakable — `{ table: {…}, th: {…}, td: {…} }`, `{ root: {…},\n * toolbar: {…} }` — and it is what a React inline-style map looks like when it\n * carries no type annotation to opt it out. Every key is a block name that is not a\n * style key, and every value is an object.\n *\n * Requiring *every* value to be an object literal is what keeps this from swallowing\n * the case it would hurt most: `const styles = { colour: 'red' }` has a string value,\n * so it is not treated as a block map and `known-property` still reports the typo.\n * Otherwise the rule that catches a misspelled property would be silenced by the\n * misspelling.\n */\nfunction isNamedCssBlockMap(node: TSESTree.ObjectExpression): boolean {\n if (node.properties.length === 0) return false;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) return false;\n\n const key = getKeyName(prop.key);\n\n if (key === null || isPlausibleStyleKey(key)) return false;\n if (prop.value.type !== 'ObjectExpression') return false;\n }\n\n return true;\n}\n\n/**\n * The component a `styles` JSX prop overrides — `<Card styles={…} />` -> `Card`.\n * A namespaced tag (`<UI.Card />`) resolves to its root object, since that is the\n * name an import can be matched against.\n */\nfunction jsxStylesPropTarget(node: TSESTree.ObjectExpression): string | null {\n const container = node.parent;\n if (container?.type !== 'JSXExpressionContainer') return null;\n\n const attribute = container.parent;\n if (attribute?.type !== 'JSXAttribute') return null;\n\n const element = attribute.parent;\n if (element?.type !== 'JSXOpeningElement') return null;\n\n let name: TSESTree.JSXTagNameExpression = element.name;\n while (name.type === 'JSXMemberExpression') name = name.object;\n\n return name.type === 'JSXIdentifier' ? name.name : null;\n}\n\n/**\n * The identifier a base-component argument resolves to — `tasty(UI.Card, {…})`\n * -> `UI`, which is what the import map is keyed by.\n *\n * Type assertions are stripped on the way down. Reading `Button as any` as an\n * unknown base would quietly promote an imported component to \"owned\", which is\n * the reading that produces a warning the author cannot act on.\n */\nfunction baseComponentName(node: TSESTree.Node): string | null {\n let current = unwrapExpression(node);\n\n while (current.type === 'MemberExpression') {\n current = unwrapExpression(current.object);\n }\n\n return current.type === 'Identifier' ? current.name : null;\n}\n\n/**\n * Whether `source` is inside this project. Relative and absolute specifiers are,\n * as are the conventional in-repo aliases (`~/`, `#internal`, `@/`) — `@` alone\n * is not, since `@scope/pkg` is a published package.\n */\nfunction isInRepoSource(source: string): boolean {\n return (\n source.startsWith('.') ||\n source.startsWith('/') ||\n source.startsWith('~') ||\n source.startsWith('#') ||\n source.startsWith('@/')\n );\n}\n\n/** Matches a source against an `ownedSources` pattern, where `*` is any run of characters. */\nfunction matchesSourcePattern(pattern: string, source: string): boolean {\n const escaped = pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, '.*')}$`).test(source);\n}\n\nconst TASTY_FUNCTION_NAMES = new Set([\n 'tasty',\n 'tastyStatic',\n 'useStyles',\n 'useGlobalStyles',\n 'configure',\n]);\n\n/**\n * Context tracker for a single file's lint pass.\n * Tracks which imports come from tasty and provides\n * helpers to determine if a node is in a tasty style context.\n */\nexport class TastyContext {\n readonly config: ResolvedConfig;\n private imports = new Map<string, TastyImport>();\n private moduleSources = new Map<string, string>();\n private importSources: Set<string>;\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n config?: ResolvedConfig,\n ) {\n this.config = config ?? loadConfig(context.filename);\n this.importSources = new Set([\n ...DEFAULT_IMPORT_SOURCES,\n ...this.config.importSources,\n ]);\n }\n\n trackImport(node: TSESTree.ImportDeclaration): void {\n const source = node.source.value;\n\n // Every import is recorded, tasty or not. Where a *base component* comes\n // from is what tells a rule whether the file it would send the author to\n // edit is even theirs — see `isOwnedComponent`.\n for (const specifier of node.specifiers) {\n this.moduleSources.set(specifier.local.name, source);\n }\n\n if (!this.importSources.has(source) || node.importKind === 'type') return;\n\n for (const specifier of node.specifiers) {\n if (\n specifier.type === 'ImportSpecifier' &&\n specifier.importKind !== 'type'\n ) {\n const importedName =\n specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n if (\n TASTY_FUNCTION_NAMES.has(importedName) ||\n Object.hasOwn(this.config.styleFunctions ?? {}, importedName)\n ) {\n this.imports.set(specifier.local.name, {\n localName: specifier.local.name,\n importedName,\n source,\n });\n }\n }\n }\n }\n\n getImport(localName: string): TastyImport | undefined {\n return this.imports.get(localName);\n }\n\n /**\n * Whether the base component an extension layer sits on top of lives in a file\n * this project can edit — so a rule can decide whether pointing the author at\n * the base's own definition is advice they can act on.\n *\n * A name that was never imported is declared in this very file, and an unknown\n * base (`null`) counts as owned as well: staying quiet is for a base that is\n * demonstrably someone else's, never for one the plugin merely failed to read.\n */\n isOwnedComponent(localName: string | null): boolean {\n if (localName === null) return true;\n\n const source = this.moduleSources.get(localName);\n if (source === undefined) return true;\n if (isInRepoSource(source)) return true;\n\n // `ownedSources` is optional on the public `ResolvedConfig`, so a config\n // built by a consumer rather than by `loadConfig` may not carry it.\n return (this.config.ownedSources ?? []).some((pattern) =>\n matchesSourcePattern(pattern, source),\n );\n }\n\n isTastyCall(node: TSESTree.CallExpression): TastyImport | undefined {\n const callee = unwrapExpression(node.callee);\n if (callee.type !== 'Identifier') return undefined;\n const imp = this.imports.get(callee.name);\n if (!imp) return undefined;\n\n // A parameter or local declaration can shadow the imported helper. Only\n // the import binding carries evidence that the call accepts Tasty styles.\n let scope = this.context.sourceCode.getScope(callee);\n while (scope) {\n const variable = scope.set.get(callee.name);\n if (variable) {\n return variable.defs.some((def) => def.type === 'ImportBinding')\n ? imp\n : undefined;\n }\n if (!scope.upper) break;\n scope = scope.upper;\n }\n return undefined;\n }\n\n /**\n * Determines whether an object expression is a tasty style object\n * by walking up the AST to find a recognized call expression.\n */\n isStyleObject(node: TSESTree.ObjectExpression): boolean {\n return this.getStyleContext(node) !== null;\n }\n\n private getSubElementParent(\n node: TSESTree.ObjectExpression,\n ): TSESTree.ObjectExpression | null {\n let wrapped: TSESTree.Node = node;\n while (wrapped.parent && unwrapExpression(wrapped.parent) === node) {\n wrapped = wrapped.parent;\n }\n const parent = wrapped.parent;\n if (parent?.type !== 'Property' || parent.computed) return null;\n const key = getKeyName(parent.key);\n return key &&\n /^[A-Z]/.test(key) &&\n parent.parent.type === 'ObjectExpression'\n ? parent.parent\n : null;\n }\n\n /** Follow only sub-elements of recognized styles, never a variants container. */\n getRootStyleObject(\n node: TSESTree.ObjectExpression,\n ): TSESTree.ObjectExpression {\n let current = node;\n while (true) {\n const parent = this.getSubElementParent(current);\n if (!parent || !this.isStyleObject(parent)) return current;\n current = parent;\n }\n }\n\n /** Include helper calls nested inside a real sub-element, but not variant names. */\n isInsideSubElement(node: TSESTree.Node): boolean {\n let current = node.parent;\n while (current) {\n if (current.type === 'Property' && !current.computed) {\n const key = getKeyName(current.key);\n if (\n key &&\n /^[A-Z]/.test(key) &&\n current.parent.type === 'ObjectExpression' &&\n this.isStyleObject(current.parent)\n ) {\n return true;\n }\n }\n current = current.parent;\n }\n return false;\n }\n\n getStyleContext(node: TSESTree.Node): StyleContext | null {\n // Sub-element objects inherit their parent style object's context\n if (node.type === 'ObjectExpression') {\n const parent = this.getSubElementParent(node);\n if (parent) {\n const inherited = this.getStyleContext(parent);\n if (inherited) return inherited;\n }\n }\n\n // An object assigned to a `styles` key (or a `styles` JSX prop) is a style\n // object on its own terms — no enclosing tasty call or `styles`-ish variable\n // name required. Checked before the walk, which would otherwise climb past it\n // to an unrelated call or declaration and reject the whole thing.\n if (node.type === 'ObjectExpression' && this.isStylesPropertyValue(node)) {\n // A `styles` prop is an override layer by construction: the component it\n // is handed to already carries its own styles and this object lands on\n // top of them. A Storybook `args.styles` is the same prop by another name,\n // but the component behind `args` is not on the AST path, so its base\n // stays unknown.\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: true,\n baseComponent: jsxStylesPropTarget(node),\n };\n }\n\n let current: TSESTree.Node | undefined = node;\n\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = this.isTastyCall(current);\n if (!imp) return null;\n\n const name = imp.importedName;\n const isStaticCall = name === 'tastyStatic';\n\n if (name === 'tasty') {\n return this.getTastyCallContext(current, node);\n }\n\n if (name === 'tastyStatic') {\n return this.getTastyStaticCallContext(current, node);\n }\n\n if (name === 'useStyles') {\n if (current.arguments[0] === node) {\n return {\n type: 'useStyles',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n baseComponent: null,\n };\n }\n }\n\n if (name === 'useGlobalStyles') {\n if (current.arguments[1] === node) {\n return {\n type: 'useGlobalStyles',\n isStaticCall,\n isSelectorMode: true,\n isExtending: false,\n baseComponent: null,\n };\n }\n }\n\n // Built-in signatures always take precedence over configured ones.\n const custom = this.config.styleFunctions?.[name];\n if (!TASTY_FUNCTION_NAMES.has(name) && custom) {\n return this.getCustomCallContext(current, node, custom);\n }\n\n return null;\n }\n\n if (this.isStyleVariableDeclaration(current, node)) {\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n baseComponent: null,\n };\n }\n\n current = current.parent;\n }\n\n return null;\n }\n\n private isStyleVariableDeclaration(\n current: TSESTree.Node,\n targetNode: TSESTree.Node,\n ): boolean {\n if (\n current.type !== 'VariableDeclarator' ||\n current.id.type !== 'Identifier'\n ) {\n return false;\n }\n\n let init: TSESTree.Node | null | undefined = current.init;\n while (\n init?.type === 'TSAsExpression' ||\n init?.type === 'TSSatisfiesExpression' ||\n init?.type === 'TSTypeAssertion' ||\n init?.type === 'TSNonNullExpression'\n ) {\n init = (init as TSESTree.TSAsExpression).expression;\n }\n\n if (init !== targetNode) return false;\n\n const name = current.id.name;\n\n if (/^[A-Z][A-Z0-9_]*$/.test(name)) return false;\n\n // An explicit type annotation is authoritative, in both directions. The name\n // heuristic below is a guess; a declared type is the author stating what the\n // object is, so it wins.\n //\n // This matters because the rewrites these rules offer are only valid *inside*\n // tasty: `#purple.05` and `$font-sans` are correct tasty declarations and\n // meaningless as raw CSS. A `const styles: Record<string, CSSProperties>`\n // handed to React's `style={…}` was matched on name alone, so its plain CSS\n // longhands were reported as tasty violations — and `--fix` rewrote real\n // `var(--shadow-sm-color)` to `#shadow-sm`, which nothing resolves there, so\n // the browser dropped the declaration.\n const annotated = this.stylesTypeAnnotation(current);\n\n if (annotated !== null) return annotated;\n\n // Only `styles` and `*Styles` count. The singular `style` is conventionally\n // a DOM inline-style object (`el.style`, `CSSProperties`, `setStyle(el, …)`)\n // holding raw CSS longhands, not tasty syntax — matching it reported those\n // longhands as tasty violations.\n if (name === 'styles' || name.endsWith('Styles')) {\n // The name is the weakest evidence there is, so let an unmistakable shape\n // overrule it. Without an annotation to go on, a map of named CSS blocks was\n // reported as tasty — `known-property` on every HTML tag name, each block read\n // as a state map — and `--fix` offered rewrites that are only valid inside\n // tasty.\n return !(\n targetNode.type === 'ObjectExpression' && isNamedCssBlockMap(targetNode)\n );\n }\n\n return false;\n }\n\n private getCustomCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n config: StyleFunctionConfig,\n ): StyleContext | null {\n const { argument, kind } = config;\n if (argument !== 'all' && (!Number.isInteger(argument) || argument < 0)) {\n return null;\n }\n // A preceding spread makes the runtime argument index unknown. Never apply\n // style rewrites to an object that might occupy a different parameter.\n if (\n argument !== 'all' &&\n call.arguments\n .slice(0, argument)\n .some((arg) => arg.type === 'SpreadElement')\n ) {\n return null;\n }\n\n const args =\n argument === 'all' ? call.arguments : [call.arguments[argument]];\n for (const arg of args) {\n if (!arg) continue;\n const value = unwrapExpression(arg);\n if (value.type !== 'ObjectExpression') continue;\n\n const isVariant =\n kind === 'options' && this.isInsideVariantsProperty(value, targetNode);\n const matches =\n kind === 'styles'\n ? value === targetNode\n : kind === 'options' &&\n (this.isInsideStylesProperty(value, targetNode) || isVariant);\n\n if (matches) {\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: !isVariant && (config.partial ?? false),\n baseComponent: null,\n };\n }\n }\n\n return null;\n }\n\n /**\n * Verdict from the declared type: `true` tasty, `false` explicitly not, `null`\n * when there is no annotation to go on and the caller should fall back to the\n * name.\n *\n * A `Styles` reference anywhere in the annotation counts, so wrappers like\n * `Styles | undefined` and `Record<string, Styles>` opt in — while\n * `CSSProperties`, `Record<string, CSSProperties>` and an inline literal of\n * them opt out.\n */\n private stylesTypeAnnotation(\n node: TSESTree.VariableDeclarator,\n ): boolean | null {\n const annotation = node.id.typeAnnotation?.typeAnnotation;\n\n if (!annotation) return null;\n\n return referencesStylesType(annotation);\n }\n\n private getTastyCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tasty({ styles: { ... } }) or tasty(Component, { styles: { ... } })\n const isExtending =\n args.length >= 2 && unwrapExpression(args[0]).type !== 'ObjectExpression';\n const optionsArg = isExtending ? args[1] : args[0];\n\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideStylesProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending,\n baseComponent: isExtending ? baseComponentName(args[0]) : null,\n };\n }\n\n // Check if inside variants\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideVariantsProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n baseComponent: null,\n };\n }\n\n return null;\n }\n\n private getTastyStaticCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tastyStatic({ ... })\n if (args.length === 1 && args[0] === targetNode) {\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode: false,\n isExtending: false,\n baseComponent: null,\n };\n }\n\n // tastyStatic(base, { ... }) or tastyStatic('selector', { ... })\n if (args.length === 2 && args[1] === targetNode) {\n const isSelectorMode = unwrapExpression(args[0]).type === 'Literal';\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode,\n isExtending: !isSelectorMode,\n baseComponent: isSelectorMode ? null : baseComponentName(args[0]),\n };\n }\n\n return null;\n }\n\n private isInsideStylesProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n !prop.computed &&\n getKeyName(prop.key) === 'styles' &&\n unwrapExpression(prop.value) === targetNode\n ) {\n return true;\n }\n }\n return false;\n }\n\n private isInsideVariantsProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n !prop.computed &&\n getKeyName(prop.key) === 'variants'\n ) {\n const variants = unwrapExpression(prop.value);\n if (variants.type !== 'ObjectExpression') continue;\n for (const variantProp of variants.properties) {\n if (\n variantProp.type === 'Property' &&\n unwrapExpression(variantProp.value) === targetNode\n ) {\n return true;\n }\n }\n }\n }\n return false;\n }\n\n /**\n * Whether this object is a `styles` JSX prop value or a Storybook `args.styles`.\n *\n * Both are unambiguous enough to stand on their own without an import-tracked\n * call or a `styles`-ish variable name. A bare `styles` key elsewhere is not —\n * see the selector list for why.\n */\n isStylesPropertyValue(node: TSESTree.ObjectExpression): boolean {\n const parent = node.parent;\n\n if (parent?.type === 'Property' && !parent.computed) {\n if (getKeyName(parent.key) !== 'styles') return false;\n\n // Only inside `args: { … }`.\n const argsObject = parent.parent;\n const argsProperty =\n argsObject?.type === 'ObjectExpression' ? argsObject.parent : undefined;\n\n return (\n argsProperty?.type === 'Property' &&\n !argsProperty.computed &&\n getKeyName(argsProperty.key) === 'args'\n );\n }\n\n if (parent?.type === 'JSXExpressionContainer') {\n const attribute = parent.parent;\n\n return (\n attribute?.type === 'JSXAttribute' &&\n attribute.name.type === 'JSXIdentifier' &&\n attribute.name.name === 'styles'\n );\n }\n\n return false;\n }\n\n /**\n * Checks if a property value node is a state mapping object\n * (i.e., an object where keys are state expressions and values are style values).\n */\n isStateMap(\n node: TSESTree.ObjectExpression,\n parentProperty: TSESTree.Property,\n ): boolean {\n const keyName = getKeyName(parentProperty.key);\n if (keyName === null) return false;\n\n // If the key starts with uppercase, it's a sub-element, not a state map\n if (/^[A-Z]/.test(keyName)) return false;\n\n // Special keys are not state maps\n // At-rule blocks are not state maps — their object values are descriptor maps.\n if (SPECIAL_STYLE_KEYS.has(keyName)) return false;\n\n // If the object has keys that look like state expressions, it's a state map.\n // A lone `_` (the fallback floor) or `''` (the default) is enough — both are\n // already covered by the string-literal / identifier branches below.\n return node.properties.some((prop) => {\n if (prop.type !== 'Property') return false;\n if (prop.key.type === 'Literal' && prop.key.value === '') return true;\n if (prop.key.type === 'Identifier') return true;\n if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') {\n return true;\n }\n return false;\n });\n }\n\n /**\n * Checks if a key represents a sub-element (starts with uppercase).\n */\n isSubElementKey(key: string): boolean {\n return /^[A-Z]/.test(key);\n }\n\n /**\n * Checks if a key represents a nested selector (starts with &).\n */\n isNestedSelectorKey(key: string): boolean {\n return key.startsWith('&');\n }\n\n /**\n * Checks if a key is a custom CSS property definition ($name or $$name).\n */\n isCustomPropertyKey(key: string): boolean {\n return key.startsWith('$');\n }\n\n /**\n * Checks if a key is a color token definition (#name or ##name).\n */\n isColorTokenKey(key: string): boolean {\n return key.startsWith('#');\n }\n\n /**\n * Checks if a key is a special @ property (@keyframes, @property, ...).\n */\n isSpecialKey(key: string): boolean {\n return key.startsWith('@');\n }\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { recommended, strict } from "./configs.js";
2
- import { ResolvedConfig, TastyValidationConfig } from "./types.js";
2
+ import { ResolvedConfig, StyleFunctionConfig, TastyValidationConfig } from "./types.js";
3
3
 
4
4
  //#region src/index.d.ts
5
5
  declare const plugin: any;
6
6
  //#endregion
7
- export { type ResolvedConfig, type TastyValidationConfig, plugin as default, recommended, strict };
7
+ export { type ResolvedConfig, type StyleFunctionConfig, type TastyValidationConfig, plugin as default, recommended, strict };
8
8
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import valid_transition_default from "./rules/valid-transition.js";
21
21
  import require_default_state_default from "./rules/require-default-state.js";
22
22
  import no_unknown_state_alias_default from "./rules/no-unknown-state-alias.js";
23
23
  import no_raw_color_values_default from "./rules/no-raw-color-values.js";
24
+ import no_raw_transition_duration_default from "./rules/no-raw-transition-duration.js";
24
25
  import no_styles_prop_default from "./rules/no-styles-prop.js";
25
26
  import consistent_token_usage_default from "./rules/consistent-token-usage.js";
26
27
  import no_runtime_styles_mutation_default from "./rules/no-runtime-styles-mutation.js";
@@ -60,6 +61,7 @@ const ruleMap = {
60
61
  "require-default-state": require_default_state_default,
61
62
  "no-unknown-state-alias": no_unknown_state_alias_default,
62
63
  "no-raw-color-values": no_raw_color_values_default,
64
+ "no-raw-transition-duration": no_raw_transition_duration_default,
63
65
  "no-styles-prop": no_styles_prop_default,
64
66
  "consistent-token-usage": consistent_token_usage_default,
65
67
  "no-runtime-styles-mutation": no_runtime_styles_mutation_default,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TastyValidationConfig, ResolvedConfig } from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,kBAAkBC;CAClB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,6BAA6BC;CAC7B,oBAAoBC;CACpB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAKD,MAAM,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAIrE,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
1
+ {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noRawTransitionDuration","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,8BAA8BC;CAC9B,kBAAkBC;CAClB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,6BAA6BC;CAC7B,oBAAoBC;CACpB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAKD,MAAM,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAIrE,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
@@ -37,6 +37,11 @@ const DIMENSION_MODS = [
37
37
  "max",
38
38
  "fixed"
39
39
  ];
40
+ const LOGICAL_EDGE_MODS = [
41
+ "start",
42
+ "end",
43
+ LONGHAND_MOD
44
+ ];
40
45
  const FLOW_MODS = [
41
46
  "row",
42
47
  "column",
@@ -110,8 +115,6 @@ const PROPERTY_EXPECTATIONS = {
110
115
  acceptsColor: false,
111
116
  acceptsMods: BOX_DIRECTIONAL_MODS
112
117
  },
113
- paddingInline: VALUE_ONLY,
114
- paddingBlock: VALUE_ONLY,
115
118
  margin: {
116
119
  acceptsColor: false,
117
120
  acceptsMods: BOX_DIRECTIONAL_MODS
@@ -124,6 +127,16 @@ const PROPERTY_EXPECTATIONS = {
124
127
  acceptsColor: false,
125
128
  acceptsMods: INSET_MODS
126
129
  },
130
+ scrollMargin: {
131
+ acceptsColor: false,
132
+ acceptsMods: BOX_DIRECTIONAL_MODS
133
+ },
134
+ scrollPadding: {
135
+ acceptsColor: false,
136
+ acceptsMods: BOX_DIRECTIONAL_MODS
137
+ },
138
+ paddingInline: VALUE_ONLY,
139
+ paddingBlock: VALUE_ONLY,
127
140
  width: {
128
141
  acceptsColor: false,
129
142
  acceptsMods: DIMENSION_MODS
@@ -132,6 +145,62 @@ const PROPERTY_EXPECTATIONS = {
132
145
  acceptsColor: false,
133
146
  acceptsMods: DIMENSION_MODS
134
147
  },
148
+ blockPadding: {
149
+ acceptsColor: false,
150
+ acceptsMods: LOGICAL_EDGE_MODS
151
+ },
152
+ inlinePadding: {
153
+ acceptsColor: false,
154
+ acceptsMods: LOGICAL_EDGE_MODS
155
+ },
156
+ blockMargin: {
157
+ acceptsColor: false,
158
+ acceptsMods: LOGICAL_EDGE_MODS
159
+ },
160
+ inlineMargin: {
161
+ acceptsColor: false,
162
+ acceptsMods: LOGICAL_EDGE_MODS
163
+ },
164
+ blockInset: {
165
+ acceptsColor: false,
166
+ acceptsMods: LOGICAL_EDGE_MODS
167
+ },
168
+ inlineInset: {
169
+ acceptsColor: false,
170
+ acceptsMods: LOGICAL_EDGE_MODS
171
+ },
172
+ blockScrollMargin: {
173
+ acceptsColor: false,
174
+ acceptsMods: LOGICAL_EDGE_MODS
175
+ },
176
+ inlineScrollMargin: {
177
+ acceptsColor: false,
178
+ acceptsMods: LOGICAL_EDGE_MODS
179
+ },
180
+ blockScrollPadding: {
181
+ acceptsColor: false,
182
+ acceptsMods: LOGICAL_EDGE_MODS
183
+ },
184
+ inlineScrollPadding: {
185
+ acceptsColor: false,
186
+ acceptsMods: LOGICAL_EDGE_MODS
187
+ },
188
+ blockBorder: {
189
+ acceptsColor: true,
190
+ acceptsMods: [...LOGICAL_EDGE_MODS, ...BORDER_STYLE_MODS]
191
+ },
192
+ inlineBorder: {
193
+ acceptsColor: true,
194
+ acceptsMods: [...LOGICAL_EDGE_MODS, ...BORDER_STYLE_MODS]
195
+ },
196
+ blockSize: {
197
+ acceptsColor: false,
198
+ acceptsMods: DIMENSION_MODS
199
+ },
200
+ inlineSize: {
201
+ acceptsColor: false,
202
+ acceptsMods: DIMENSION_MODS
203
+ },
135
204
  gap: VALUE_ONLY,
136
205
  columnGap: VALUE_ONLY,
137
206
  rowGap: VALUE_ONLY,