@tenphi/eslint-plugin-tasty 1.12.0 → 1.12.1
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 +12 -2
- package/dist/constants.js +8 -32
- package/dist/constants.js.map +1 -1
- package/dist/context.js +92 -42
- package/dist/context.js.map +1 -1
- package/dist/parsers/value-parser.js +6 -0
- package/dist/parsers/value-parser.js.map +1 -1
- package/dist/property-expectations.js +7 -2
- package/dist/property-expectations.js.map +1 -1
- package/dist/protected-components.js +4 -3
- package/dist/protected-components.js.map +1 -1
- package/dist/rules/valid-directional-modifier.js +1 -0
- package/dist/rules/valid-directional-modifier.js.map +1 -1
- package/dist/rules/valid-value.js +1 -1
- package/dist/rules/valid-value.js.map +1 -1
- package/dist/style-values.js +6 -1
- package/dist/style-values.js.map +1 -1
- package/dist/type-aware-jsx.js +10 -2
- package/dist/type-aware-jsx.js.map +1 -1
- package/dist/utils.js +10 -1
- package/dist/utils.js.map +1 -1
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protected-components.js","names":[],"sources":["../src/protected-components.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext, Scope } from '@typescript-eslint/utils/ts-eslint';\nimport type { TastyContext } from './context.js';\nimport type { ResolvedConfig } from './types.js';\nimport { DIMENSION_STYLES, POSITION_STYLES } from './constants.js';\nimport {\n type ModuleExportOrigin,\n resolveModuleExportOrigin,\n withMembers,\n} from './module-components.js';\nimport { unwrapExpression, wildcardPattern } from './utils.js';\n\n/** A component resolved to a `protectedComponents` entry. */\nexport interface ProtectedComponent {\n /**\n * The name it is protected under: the export name (`Button` for `<UI.Button>`,\n * an alias or `<Button.Split>`), or the member path an entry names, such as\n * `Button.Group`.\n */\n name: string;\n source: string;\n /** Whether a `tasty()` wrapper sits between the reference and the export. */\n wrapped: boolean;\n allowedStyleProps: ReadonlySet<string>;\n}\n\ninterface Pattern {\n regex: RegExp;\n /** Names a member path, such as `Button.Group` or `Button.*`. */\n member: boolean;\n}\n\ninterface Entry {\n source: RegExp;\n include: Pattern[];\n exclude: Pattern[];\n allowedStyleProps: ReadonlySet<string>;\n}\n\nfunction namePattern(pattern: string): Pattern {\n return { regex: wildcardPattern(pattern), member: pattern.includes('.') };\n}\n\nconst STYLE_PROP_GROUPS = new Map<string, readonly string[]>([\n ['position', POSITION_STYLES],\n ['dimension', DIMENSION_STYLES],\n]);\n\nconst entriesByConfig = new WeakMap<ResolvedConfig, Entry[]>();\n\nfunction readEntries(config: ResolvedConfig): Entry[] {\n let entries = entriesByConfig.get(config);\n if (entries) return entries;\n\n const list: unknown = config.protectedComponents ?? [];\n if (!Array.isArray(list)) {\n throw new Error('tasty: protectedComponents must be an array.');\n }\n entries = list.map((item: unknown, index): Entry => {\n const at = `tasty: protectedComponents[${index}]`;\n if (\n !item ||\n typeof item !== 'object' ||\n typeof (item as { source?: unknown }).source !== 'string'\n ) {\n throw new Error(`${at} must be an object with a string 'source'.`);\n }\n const entry = item as Record<string, unknown>;\n const strings = (key: string): string[] => {\n const value = entry[key];\n if (value === undefined) return key === 'include' ? ['*'] : [];\n if (\n !Array.isArray(value) ||\n value.some((name) => typeof name !== 'string')\n ) {\n throw new Error(`${at}.${key} must be an array of strings.`);\n }\n return value as string[];\n };\n return {\n source: wildcardPattern(entry.source as string),\n include: strings('include').map(namePattern),\n exclude: strings('exclude').map(namePattern),\n allowedStyleProps: new Set(\n strings('allowedStyleProps').flatMap(\n (name) => STYLE_PROP_GROUPS.get(name) ?? [name],\n ),\n ),\n };\n });\n entriesByConfig.set(config, entries);\n return entries;\n}\n\nfunction importedName(specifier: TSESTree.ImportSpecifier): string {\n return specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n}\n\n/**\n * Resolves JSX tags and `tasty()` bases to the `protectedComponents` entry they\n * name, by the name their source exports them under plus any members read off\n * it (`Button.Group`). Aliases, namespace members (`UI.Button`), `const`\n * aliases, `tasty()` wrappers and relative re-exports are followed; relative\n * modules need the `typescript` package, as for other cross-file recognition.\n */\nexport class ProtectedComponents {\n private entries: Entry[];\n private bindings = new WeakMap<\n Scope.Variable,\n Map<string, ModuleExportOrigin | null>\n >();\n private modules = new Map<string, ModuleExportOrigin | null>();\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n private ctx: TastyContext,\n ) {\n this.entries = readEntries(ctx.config);\n }\n\n get enabled(): boolean {\n return this.entries.length > 0;\n }\n\n /** `tasty(Base, ...)`: a Tasty wrapper of an existing component. */\n isWrapperCall(node: TSESTree.CallExpression): boolean {\n const base = node.arguments[0];\n return (\n this.ctx.isTastyCall(node)?.importedName === 'tasty' &&\n base !== undefined &&\n base.type !== 'SpreadElement' &&\n unwrapExpression(base).type !== 'ObjectExpression'\n );\n }\n\n /** The protected component a JSX tag name refers to. */\n resolveTag(name: TSESTree.JSXTagNameExpression): ProtectedComponent | null {\n // `<button>` is a native element whatever a `button` binding holds.\n if (name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name)) return null;\n let root = name;\n const members: string[] = [];\n while (root.type === 'JSXMemberExpression') {\n members.unshift(root.property.name);\n root = root.object;\n }\n if (root.type !== 'JSXIdentifier') return null;\n return this.match(this.resolveBinding(root, root.name, members, new Set()));\n }\n\n /** The protected component an expression, such as a `tasty()` base, refers to. */\n resolveExpression(node: TSESTree.Node): ProtectedComponent | null {\n return this.match(this.origin(node, [], new Set()));\n }\n\n /**\n * The most specific name decides: an entry that names a member path\n * (`Button.Group`, `Button.*`) in `include` or `exclude` wins over one that\n * matches only the export, so a sub-component that is a separate component\n * can follow its own entry. Longer paths are tried first.\n */\n private match(origin: ModuleExportOrigin | null): ProtectedComponent | null {\n if (!origin) return null;\n const entries = this.entries.filter((entry) =>\n entry.source.test(origin.source),\n );\n const protect = (name: string, entry: Entry): ProtectedComponent => ({\n name,\n source: origin.source,\n wrapped: origin.wrapped,\n allowedStyleProps: entry.allowedStyleProps,\n });\n\n const parts = [origin.name, ...origin.members];\n for (let length = parts.length; length > 1; length--) {\n const path = parts.slice(0, length).join('.');\n const names = (patterns: Pattern[]) =>\n patterns.some((pattern) => pattern.member && pattern.regex.test(path));\n for (const entry of entries) {\n if (names(entry.exclude)) return null;\n if (names(entry.include)) return protect(path, entry);\n }\n }\n\n const matches = (patterns: Pattern[]) =>\n patterns.some(\n (pattern) => !pattern.member && pattern.regex.test(origin.name),\n );\n const entry = entries.find(\n (candidate) => matches(candidate.include) && !matches(candidate.exclude),\n );\n return entry ? protect(origin.name, entry) : null;\n }\n\n private origin(\n node: TSESTree.Node,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n const value = unwrapExpression(node);\n if (value.type === 'Identifier') {\n return this.resolveBinding(value, value.name, members, visited);\n }\n if (value.type === 'MemberExpression') {\n let root: TSESTree.Node = value;\n const path = [...members];\n while (root.type === 'MemberExpression') {\n if (root.computed || root.property.type !== 'Identifier') return null;\n path.unshift(root.property.name);\n root = unwrapExpression(root.object);\n }\n return root.type === 'Identifier'\n ? this.resolveBinding(root, root.name, path, visited)\n : null;\n }\n if (\n members.length === 0 &&\n value.type === 'CallExpression' &&\n this.isWrapperCall(value)\n ) {\n const origin = this.origin(value.arguments[0], [], visited);\n return origin && { ...origin, wrapped: true };\n }\n return null;\n }\n\n /** `members` are the properties read off the binding, as in `UI.Button.Group`. */\n private resolveBinding(\n reference: TSESTree.Node,\n localName: string,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n let scope: Scope.Scope | null = this.context.sourceCode.getScope(reference);\n while (scope && !scope.set.has(localName)) scope = scope.upper;\n const variable = scope?.set.get(localName);\n if (!variable || variable.defs.length !== 1) return null;\n\n let cache = this.bindings.get(variable);\n if (!cache) {\n cache = new Map();\n this.bindings.set(variable, cache);\n }\n const key = members.join('.');\n if (cache.has(key)) return cache.get(key)!;\n\n const result = this.resolveDefinition(variable.defs[0], members, visited);\n cache.set(key, result);\n return result;\n }\n\n private resolveDefinition(\n def: Scope.Definition,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n if (def.type === 'ImportBinding') {\n if (\n def.parent.type !== 'ImportDeclaration' ||\n def.parent.importKind === 'type'\n ) {\n return null;\n }\n const specifier = def.node;\n let name: string | undefined;\n let rest = members;\n if (specifier.type === 'ImportNamespaceSpecifier') {\n [name, ...rest] = members;\n } else if (specifier.type === 'ImportDefaultSpecifier') name = 'default';\n else if (specifier.type === 'ImportSpecifier') {\n if (specifier.importKind === 'type') return null;\n name = importedName(specifier);\n }\n if (name === undefined) return null;\n\n const source = def.parent.source.value;\n if (!source.startsWith('.')) {\n return { source, name, members: rest, wrapped: false };\n }\n return withMembers(this.moduleOrigin(source, name), rest);\n }\n\n if (\n def.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n def.node.id.type !== 'Identifier' ||\n !def.node.init ||\n visited.has(def.node)\n ) {\n return null;\n }\n visited.add(def.node);\n return this.origin(def.node.init, members, visited);\n }\n\n private moduleOrigin(\n source: string,\n exportName: string,\n ): ModuleExportOrigin | null {\n const key = `${source}\\0${exportName}`;\n if (!this.modules.has(key)) {\n this.modules.set(\n key,\n resolveModuleExportOrigin(this.context.filename, source, exportName, {\n importSources: this.ctx.trackedSources,\n isComponentFactory: (name) => name === 'tasty',\n isOriginSource: (candidate) =>\n this.entries.some((entry) => entry.source.test(candidate)),\n }),\n );\n }\n return this.modules.get(key)!;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"protected-components.js","names":[],"sources":["../src/protected-components.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext, Scope } from '@typescript-eslint/utils/ts-eslint';\nimport type { TastyContext } from './context.js';\nimport type { ResolvedConfig } from './types.js';\nimport { DIMENSION_STYLES, POSITION_STYLES } from './constants.js';\nimport {\n type ModuleExportOrigin,\n resolveModuleExportOrigin,\n withMembers,\n} from './module-components.js';\nimport { unwrapExpression, valueDefinition, wildcardPattern } from './utils.js';\n\n/** A component resolved to a `protectedComponents` entry. */\nexport interface ProtectedComponent {\n /**\n * The name it is protected under: the export name (`Button` for `<UI.Button>`,\n * an alias or `<Button.Split>`), or the member path an entry names, such as\n * `Button.Group`.\n */\n name: string;\n source: string;\n /** Whether a `tasty()` wrapper sits between the reference and the export. */\n wrapped: boolean;\n allowedStyleProps: ReadonlySet<string>;\n}\n\ninterface Pattern {\n regex: RegExp;\n /** Names a member path, such as `Button.Group` or `Button.*`. */\n member: boolean;\n}\n\ninterface Entry {\n source: RegExp;\n include: Pattern[];\n exclude: Pattern[];\n allowedStyleProps: ReadonlySet<string>;\n}\n\nfunction namePattern(pattern: string): Pattern {\n return { regex: wildcardPattern(pattern), member: pattern.includes('.') };\n}\n\nconst STYLE_PROP_GROUPS = new Map<string, readonly string[]>([\n ['position', POSITION_STYLES],\n ['dimension', DIMENSION_STYLES],\n]);\n\nconst entriesByConfig = new WeakMap<ResolvedConfig, Entry[]>();\n\nfunction readEntries(config: ResolvedConfig): Entry[] {\n let entries = entriesByConfig.get(config);\n if (entries) return entries;\n\n const list: unknown = config.protectedComponents ?? [];\n if (!Array.isArray(list)) {\n throw new Error('tasty: protectedComponents must be an array.');\n }\n entries = list.map((item: unknown, index): Entry => {\n const at = `tasty: protectedComponents[${index}]`;\n if (\n !item ||\n typeof item !== 'object' ||\n typeof (item as { source?: unknown }).source !== 'string'\n ) {\n throw new Error(`${at} must be an object with a string 'source'.`);\n }\n const entry = item as Record<string, unknown>;\n const strings = (key: string): string[] => {\n const value = entry[key];\n if (value === undefined) return key === 'include' ? ['*'] : [];\n if (\n !Array.isArray(value) ||\n value.some((name) => typeof name !== 'string')\n ) {\n throw new Error(`${at}.${key} must be an array of strings.`);\n }\n return value as string[];\n };\n return {\n source: wildcardPattern(entry.source as string),\n include: strings('include').map(namePattern),\n exclude: strings('exclude').map(namePattern),\n allowedStyleProps: new Set(\n strings('allowedStyleProps').flatMap(\n (name) => STYLE_PROP_GROUPS.get(name) ?? [name],\n ),\n ),\n };\n });\n entriesByConfig.set(config, entries);\n return entries;\n}\n\nfunction importedName(specifier: TSESTree.ImportSpecifier): string {\n return specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n}\n\n/**\n * Resolves JSX tags and `tasty()` bases to the `protectedComponents` entry they\n * name, by the name their source exports them under plus any members read off\n * it (`Button.Group`). Aliases, namespace members (`UI.Button`), `const`\n * aliases, `tasty()` wrappers and relative re-exports are followed; relative\n * modules need the `typescript` package, as for other cross-file recognition.\n */\nexport class ProtectedComponents {\n private entries: Entry[];\n private bindings = new WeakMap<\n Scope.Variable,\n Map<string, ModuleExportOrigin | null>\n >();\n private modules = new Map<string, ModuleExportOrigin | null>();\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n private ctx: TastyContext,\n ) {\n this.entries = readEntries(ctx.config);\n }\n\n get enabled(): boolean {\n return this.entries.length > 0;\n }\n\n /** `tasty(Base, ...)`: a Tasty wrapper of an existing component. */\n isWrapperCall(node: TSESTree.CallExpression): boolean {\n const base = node.arguments[0];\n return (\n this.ctx.isTastyCall(node)?.importedName === 'tasty' &&\n base !== undefined &&\n base.type !== 'SpreadElement' &&\n unwrapExpression(base).type !== 'ObjectExpression'\n );\n }\n\n /** The protected component a JSX tag name refers to. */\n resolveTag(name: TSESTree.JSXTagNameExpression): ProtectedComponent | null {\n // `<button>` is a native element whatever a `button` binding holds.\n if (name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name)) return null;\n let root = name;\n const members: string[] = [];\n while (root.type === 'JSXMemberExpression') {\n members.unshift(root.property.name);\n root = root.object;\n }\n if (root.type !== 'JSXIdentifier') return null;\n return this.match(this.resolveBinding(root, root.name, members, new Set()));\n }\n\n /** The protected component an expression, such as a `tasty()` base, refers to. */\n resolveExpression(node: TSESTree.Node): ProtectedComponent | null {\n return this.match(this.origin(node, [], new Set()));\n }\n\n /**\n * The most specific name decides: an entry that names a member path\n * (`Button.Group`, `Button.*`) in `include` or `exclude` wins over one that\n * matches only the export, so a sub-component that is a separate component\n * can follow its own entry. Longer paths are tried first.\n */\n private match(origin: ModuleExportOrigin | null): ProtectedComponent | null {\n if (!origin) return null;\n const entries = this.entries.filter((entry) =>\n entry.source.test(origin.source),\n );\n const protect = (name: string, entry: Entry): ProtectedComponent => ({\n name,\n source: origin.source,\n wrapped: origin.wrapped,\n allowedStyleProps: entry.allowedStyleProps,\n });\n\n const parts = [origin.name, ...origin.members];\n for (let length = parts.length; length > 1; length--) {\n const path = parts.slice(0, length).join('.');\n const names = (patterns: Pattern[]) =>\n patterns.some((pattern) => pattern.member && pattern.regex.test(path));\n for (const entry of entries) {\n if (names(entry.exclude)) return null;\n if (names(entry.include)) return protect(path, entry);\n }\n }\n\n const matches = (patterns: Pattern[]) =>\n patterns.some(\n (pattern) => !pattern.member && pattern.regex.test(origin.name),\n );\n const entry = entries.find(\n (candidate) => matches(candidate.include) && !matches(candidate.exclude),\n );\n return entry ? protect(origin.name, entry) : null;\n }\n\n private origin(\n node: TSESTree.Node,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n const value = unwrapExpression(node);\n if (value.type === 'Identifier') {\n return this.resolveBinding(value, value.name, members, visited);\n }\n if (value.type === 'MemberExpression') {\n let root: TSESTree.Node = value;\n const path = [...members];\n while (root.type === 'MemberExpression') {\n if (root.computed || root.property.type !== 'Identifier') return null;\n path.unshift(root.property.name);\n root = unwrapExpression(root.object);\n }\n return root.type === 'Identifier'\n ? this.resolveBinding(root, root.name, path, visited)\n : null;\n }\n if (\n members.length === 0 &&\n value.type === 'CallExpression' &&\n this.isWrapperCall(value)\n ) {\n const origin = this.origin(value.arguments[0], [], visited);\n return origin && { ...origin, wrapped: true };\n }\n return null;\n }\n\n /** `members` are the properties read off the binding, as in `UI.Button.Group`. */\n private resolveBinding(\n reference: TSESTree.Node,\n localName: string,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n let scope: Scope.Scope | null = this.context.sourceCode.getScope(reference);\n while (scope && !scope.set.has(localName)) scope = scope.upper;\n const variable = scope?.set.get(localName);\n const def = variable && valueDefinition(variable);\n if (!variable || !def) return null;\n\n let cache = this.bindings.get(variable);\n if (!cache) {\n cache = new Map();\n this.bindings.set(variable, cache);\n }\n const key = members.join('.');\n if (cache.has(key)) return cache.get(key)!;\n\n const result = this.resolveDefinition(def, members, visited);\n cache.set(key, result);\n return result;\n }\n\n private resolveDefinition(\n def: Scope.Definition,\n members: string[],\n visited: Set<TSESTree.Node>,\n ): ModuleExportOrigin | null {\n if (def.type === 'ImportBinding') {\n if (\n def.parent.type !== 'ImportDeclaration' ||\n def.parent.importKind === 'type'\n ) {\n return null;\n }\n const specifier = def.node;\n let name: string | undefined;\n let rest = members;\n if (specifier.type === 'ImportNamespaceSpecifier') {\n [name, ...rest] = members;\n } else if (specifier.type === 'ImportDefaultSpecifier') name = 'default';\n else if (specifier.type === 'ImportSpecifier') {\n if (specifier.importKind === 'type') return null;\n name = importedName(specifier);\n }\n if (name === undefined) return null;\n\n const source = def.parent.source.value;\n if (!source.startsWith('.')) {\n return { source, name, members: rest, wrapped: false };\n }\n return withMembers(this.moduleOrigin(source, name), rest);\n }\n\n if (\n def.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n def.node.id.type !== 'Identifier' ||\n !def.node.init ||\n visited.has(def.node)\n ) {\n return null;\n }\n visited.add(def.node);\n return this.origin(def.node.init, members, visited);\n }\n\n private moduleOrigin(\n source: string,\n exportName: string,\n ): ModuleExportOrigin | null {\n const key = `${source}\\0${exportName}`;\n if (!this.modules.has(key)) {\n this.modules.set(\n key,\n resolveModuleExportOrigin(this.context.filename, source, exportName, {\n importSources: this.ctx.trackedSources,\n isComponentFactory: (name) => name === 'tasty',\n isOriginSource: (candidate) =>\n this.entries.some((entry) => entry.source.test(candidate)),\n }),\n );\n }\n return this.modules.get(key)!;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-directional-modifier.js","names":[],"sources":["../../src/rules/valid-directional-modifier.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport {\n DIRECTIONAL_MODIFIERS,\n DOCK_PROPERTIES,\n SINGLE_VALUE_DIRECTIONAL_PROPERTIES,\n} from '../constants.js';\nimport { parseValue } from '../parsers/index.js';\nimport type { ValueToken } from '../parsers/index.js';\n\ntype MessageIds = 'invalidDirectionalModifier' | 'tooManyValues';\n\n/**\n * Every direction word the rule recognises, across physical and logical\n * vocabularies. A word here is read as a *modifier*; whether it is accepted is\n * then decided per property by `DIRECTIONAL_MODIFIERS`.\n *\n * `start` and `end` are ordinary CSS *values* for a long tail of properties\n * (`textAlign: 'start'`, `justifyContent: 'end'`), which is exactly what the\n * `DIRECTIONAL_MODIFIERS` gate in `checkValue` exists to keep out: only the\n * properties with a directional vocabulary are ever inspected. Inside that set\n * no property takes `start`/`end` as a value, so reading them as modifiers is\n * safe — and it is what turns `padding: '1x start'` (a physical property given\n * a logical edge, which the handler drops) into a report.\n */\nconst ALL_DIRECTIONS = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n 'start',\n 'end',\n]);\n\n/**\n * Token types that count as a *value* rather than a modifier.\n *\n * Deliberately excludes `keyword`: direction words, `solid`, `inherit` and every\n * other bare word classify as keywords, and a CSS-wide keyword alongside a\n * direction (`padding: 'inherit top'`) is legal while carrying no value of its own.\n *\n * Also excludes colors: a `fade` group carries a width plus up to two mask\n * colors, so colors must not count toward the one-value limit.\n */\nconst VALUE_TOKEN_TYPES = new Set<ValueToken['type']>([\n 'custom-unit',\n 'css-unit',\n 'number',\n 'custom-prop',\n 'custom-prop-ref',\n 'css-function',\n 'tasty-function',\n 'group-expr',\n]);\n\n/** The source text of a token. `keyword` tokens carry `value` rather than `raw`. */\nfunction tokenText(token: ValueToken): string {\n if ('raw' in token && typeof token.raw === 'string') return token.raw;\n if (token.type === 'keyword') return token.value;\n\n return '';\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-directional-modifier',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate directional modifiers: only on properties that support them, and one value per group',\n },\n messages: {\n invalidDirectionalModifier:\n \"Modifier '{{modifier}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n tooManyValues:\n 'A group that names directions takes {{limit}}, but \\'{{group}}\\' has {{count}}. Values and modifiers are parsed into separate buckets, so their order is not recoverable — use comma-separated groups for per-side values, e.g. \"2x top, 4x right\".',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const allowedMods = DIRECTIONAL_MODIFIERS[property];\n // Only properties with a directional vocabulary are checked.\n //\n // This gate looks like it makes the \"wrong property\" case unreachable, and\n // it does — but removing it is not the fix. Direction words are ordinary CSS\n // *values* for a long tail of properties: `verticalAlign: 'bottom'`,\n // `textAlign: 'left'`, `transformOrigin: 'top center'`,\n // `backgroundPosition`, `objectPosition`, `float`, `clear`,\n // `scrollSnapAlign`, and `transition` (whose value names properties, which\n // can themselves be `left`/`top`). Checking every property reported all of\n // those as errors across a real design system. Catching `fill: '#purple\n // top'` is not worth an allowlist of every property that takes a\n // positional keyword.\n if (!allowedMods) return;\n\n // `parseValue` splits on commas (groups) and spaced slashes (parts), which\n // is what makes a per-group arity check possible at all.\n const { groups } = parseValue(value, {\n skipUnitValidation: true,\n skipFuncValidation: true,\n });\n\n for (const group of groups) {\n const tokens = group.parts.flatMap((part) => part.tokens);\n const directions: string[] = [];\n let valueCount = 0;\n let hasSpanModifier = false;\n\n for (const token of tokens) {\n if (VALUE_TOKEN_TYPES.has(token.type)) {\n valueCount++;\n continue;\n }\n\n const word = tokenText(token);\n\n if (ALL_DIRECTIONS.has(word)) {\n directions.push(word);\n } else if (word === 'dock' && DOCK_PROPERTIES.has(property)) {\n hasSpanModifier = true;\n }\n }\n\n if (directions.length === 0) continue;\n\n let reportedUnknown = false;\n for (const direction of directions) {\n if (!allowedMods.has(direction)) {\n context.report({\n node,\n messageId: 'invalidDirectionalModifier',\n data: {\n property,\n modifier: direction,\n accepted: [...allowedMods].join(', '),\n },\n });\n reportedUnknown = true;\n }\n }\n\n // Don't pile an arity complaint on top of an already-invalid modifier.\n if (reportedUnknown) continue;\n\n if (!SINGLE_VALUE_DIRECTIONAL_PROPERTIES.has(property)) continue;\n\n // Values and modifiers are bucketed separately by the parser, so the\n // pairing a reader infers from source order does not survive parsing: a\n // group naming directions carries one value, applied to every direction it\n // names. `dock` is the exception — its second value insets the sides the\n // dock spans.\n const limit = hasSpanModifier ? 2 : 1;\n\n if (valueCount > limit) {\n context.report({\n node,\n messageId: 'tooManyValues',\n data: {\n group: tokens.map(tokenText).filter(Boolean).join(' '),\n count: `${valueCount}`,\n limit: hasSpanModifier ? 'at most two values' : 'a single value',\n },\n });\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n checkValue(property, value, node);\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;AAYF,MAAM,oBAAoB,IAAI,IAAwB;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,SAAS,UAAU,OAA2B;AAC5C,KAAI,SAAS,SAAS,OAAO,MAAM,QAAQ,SAAU,QAAO,MAAM;AAClE,KAAI,MAAM,SAAS,UAAW,QAAO,MAAM;AAE3C,QAAO;;AAGT,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iGACH;EACD,UAAU;GACR,4BACE;GACF,eACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,cAAc,sBAAsB;AAa1C,OAAI,CAAC,YAAa;GAIlB,MAAM,EAAE,WAAW,WAAW,OAAO;IACnC,oBAAoB;IACpB,oBAAoB;IACrB,CAAC;AAEF,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,SAAS,MAAM,MAAM,SAAS,SAAS,KAAK,OAAO;IACzD,MAAM,aAAuB,EAAE;IAC/B,IAAI,aAAa;IACjB,IAAI,kBAAkB;AAEtB,SAAK,MAAM,SAAS,QAAQ;AAC1B,SAAI,kBAAkB,IAAI,MAAM,KAAK,EAAE;AACrC;AACA;;KAGF,MAAM,OAAO,UAAU,MAAM;AAE7B,SAAI,eAAe,IAAI,KAAK,CAC1B,YAAW,KAAK,KAAK;cACZ,SAAS,UAAU,gBAAgB,IAAI,SAAS,CACzD,mBAAkB;;AAItB,QAAI,WAAW,WAAW,EAAG;IAE7B,IAAI,kBAAkB;AACtB,SAAK,MAAM,aAAa,WACtB,KAAI,CAAC,YAAY,IAAI,UAAU,EAAE;AAC/B,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OACJ;OACA,UAAU;OACV,UAAU,CAAC,GAAG,YAAY,CAAC,KAAK,KAAK;OACtC;MACF,CAAC;AACF,uBAAkB;;AAKtB,QAAI,gBAAiB;AAErB,QAAI,CAAC,oCAAoC,IAAI,SAAS,CAAE;AASxD,QAAI,cAFU,kBAAkB,IAAI,GAGlC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ,OAAO,OAAO,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;MACtD,OAAO,GAAG;MACV,OAAO,kBAAkB,uBAAuB;MACjD;KACF,CAAC;;;AAKR,SAAO,0BAA0B,MAAM,OAAO,MAAM,aAAa;AAC/D,cAAW,UAAU,OAAO,KAAK;IACjC;;CAEL,CAAC"}
|
|
1
|
+
{"version":3,"file":"valid-directional-modifier.js","names":[],"sources":["../../src/rules/valid-directional-modifier.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport {\n DIRECTIONAL_MODIFIERS,\n DOCK_PROPERTIES,\n SINGLE_VALUE_DIRECTIONAL_PROPERTIES,\n} from '../constants.js';\nimport { parseValue } from '../parsers/index.js';\nimport type { ValueToken } from '../parsers/index.js';\n\ntype MessageIds = 'invalidDirectionalModifier' | 'tooManyValues';\n\n/**\n * Every direction word the rule recognises, across physical and logical\n * vocabularies. A word here is read as a *modifier*; whether it is accepted is\n * then decided per property by `DIRECTIONAL_MODIFIERS`.\n *\n * `start` and `end` are ordinary CSS *values* for a long tail of properties\n * (`textAlign: 'start'`, `justifyContent: 'end'`), which is exactly what the\n * `DIRECTIONAL_MODIFIERS` gate in `checkValue` exists to keep out: only the\n * properties with a directional vocabulary are ever inspected. Inside that set\n * no property takes `start`/`end` as a value, so reading them as modifiers is\n * safe — and it is what turns `padding: '1x start'` (a physical property given\n * a logical edge, which the handler drops) into a report.\n */\nconst ALL_DIRECTIONS = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n 'start',\n 'end',\n]);\n\n/**\n * Token types that count as a *value* rather than a modifier.\n *\n * Deliberately excludes `keyword`: direction words, `solid`, `inherit` and every\n * other bare word classify as keywords, and a CSS-wide keyword alongside a\n * direction (`padding: 'inherit top'`) is legal while carrying no value of its own.\n *\n * Also excludes colors: a `fade` group carries a width plus up to two mask\n * colors, so colors must not count toward the one-value limit.\n */\nconst VALUE_TOKEN_TYPES = new Set<ValueToken['type']>([\n 'custom-unit',\n 'css-unit',\n 'number',\n 'ratio',\n 'custom-prop',\n 'custom-prop-ref',\n 'css-function',\n 'tasty-function',\n 'group-expr',\n]);\n\n/** The source text of a token. `keyword` tokens carry `value` rather than `raw`. */\nfunction tokenText(token: ValueToken): string {\n if ('raw' in token && typeof token.raw === 'string') return token.raw;\n if (token.type === 'keyword') return token.value;\n\n return '';\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-directional-modifier',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate directional modifiers: only on properties that support them, and one value per group',\n },\n messages: {\n invalidDirectionalModifier:\n \"Modifier '{{modifier}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n tooManyValues:\n 'A group that names directions takes {{limit}}, but \\'{{group}}\\' has {{count}}. Values and modifiers are parsed into separate buckets, so their order is not recoverable — use comma-separated groups for per-side values, e.g. \"2x top, 4x right\".',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const allowedMods = DIRECTIONAL_MODIFIERS[property];\n // Only properties with a directional vocabulary are checked.\n //\n // This gate looks like it makes the \"wrong property\" case unreachable, and\n // it does — but removing it is not the fix. Direction words are ordinary CSS\n // *values* for a long tail of properties: `verticalAlign: 'bottom'`,\n // `textAlign: 'left'`, `transformOrigin: 'top center'`,\n // `backgroundPosition`, `objectPosition`, `float`, `clear`,\n // `scrollSnapAlign`, and `transition` (whose value names properties, which\n // can themselves be `left`/`top`). Checking every property reported all of\n // those as errors across a real design system. Catching `fill: '#purple\n // top'` is not worth an allowlist of every property that takes a\n // positional keyword.\n if (!allowedMods) return;\n\n // `parseValue` splits on commas (groups) and spaced slashes (parts), which\n // is what makes a per-group arity check possible at all.\n const { groups } = parseValue(value, {\n skipUnitValidation: true,\n skipFuncValidation: true,\n });\n\n for (const group of groups) {\n const tokens = group.parts.flatMap((part) => part.tokens);\n const directions: string[] = [];\n let valueCount = 0;\n let hasSpanModifier = false;\n\n for (const token of tokens) {\n if (VALUE_TOKEN_TYPES.has(token.type)) {\n valueCount++;\n continue;\n }\n\n const word = tokenText(token);\n\n if (ALL_DIRECTIONS.has(word)) {\n directions.push(word);\n } else if (word === 'dock' && DOCK_PROPERTIES.has(property)) {\n hasSpanModifier = true;\n }\n }\n\n if (directions.length === 0) continue;\n\n let reportedUnknown = false;\n for (const direction of directions) {\n if (!allowedMods.has(direction)) {\n context.report({\n node,\n messageId: 'invalidDirectionalModifier',\n data: {\n property,\n modifier: direction,\n accepted: [...allowedMods].join(', '),\n },\n });\n reportedUnknown = true;\n }\n }\n\n // Don't pile an arity complaint on top of an already-invalid modifier.\n if (reportedUnknown) continue;\n\n if (!SINGLE_VALUE_DIRECTIONAL_PROPERTIES.has(property)) continue;\n\n // Values and modifiers are bucketed separately by the parser, so the\n // pairing a reader infers from source order does not survive parsing: a\n // group naming directions carries one value, applied to every direction it\n // names. `dock` is the exception — its second value insets the sides the\n // dock spans.\n const limit = hasSpanModifier ? 2 : 1;\n\n if (valueCount > limit) {\n context.report({\n node,\n messageId: 'tooManyValues',\n data: {\n group: tokens.map(tokenText).filter(Boolean).join(' '),\n count: `${valueCount}`,\n limit: hasSpanModifier ? 'at most two values' : 'a single value',\n },\n });\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n checkValue(property, value, node);\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;AAYF,MAAM,oBAAoB,IAAI,IAAwB;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,SAAS,UAAU,OAA2B;AAC5C,KAAI,SAAS,SAAS,OAAO,MAAM,QAAQ,SAAU,QAAO,MAAM;AAClE,KAAI,MAAM,SAAS,UAAW,QAAO,MAAM;AAE3C,QAAO;;AAGT,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iGACH;EACD,UAAU;GACR,4BACE;GACF,eACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,cAAc,sBAAsB;AAa1C,OAAI,CAAC,YAAa;GAIlB,MAAM,EAAE,WAAW,WAAW,OAAO;IACnC,oBAAoB;IACpB,oBAAoB;IACrB,CAAC;AAEF,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,SAAS,MAAM,MAAM,SAAS,SAAS,KAAK,OAAO;IACzD,MAAM,aAAuB,EAAE;IAC/B,IAAI,aAAa;IACjB,IAAI,kBAAkB;AAEtB,SAAK,MAAM,SAAS,QAAQ;AAC1B,SAAI,kBAAkB,IAAI,MAAM,KAAK,EAAE;AACrC;AACA;;KAGF,MAAM,OAAO,UAAU,MAAM;AAE7B,SAAI,eAAe,IAAI,KAAK,CAC1B,YAAW,KAAK,KAAK;cACZ,SAAS,UAAU,gBAAgB,IAAI,SAAS,CACzD,mBAAkB;;AAItB,QAAI,WAAW,WAAW,EAAG;IAE7B,IAAI,kBAAkB;AACtB,SAAK,MAAM,aAAa,WACtB,KAAI,CAAC,YAAY,IAAI,UAAU,EAAE;AAC/B,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OACJ;OACA,UAAU;OACV,UAAU,CAAC,GAAG,YAAY,CAAC,KAAK,KAAK;OACtC;MACF,CAAC;AACF,uBAAkB;;AAKtB,QAAI,gBAAiB;AAErB,QAAI,CAAC,oCAAoC,IAAI,SAAS,CAAE;AASxD,QAAI,cAFU,kBAAkB,IAAI,GAGlC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ,OAAO,OAAO,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;MACtD,OAAO,GAAG;MACV,OAAO,kBAAkB,uBAAuB;MACjD;KACF,CAAC;;;AAKR,SAAO,0BAA0B,MAAM,OAAO,MAAM,aAAa;AAC/D,cAAW,UAAU,OAAO,KAAK;IACjC;;CAEL,CAAC"}
|
|
@@ -112,7 +112,7 @@ var valid_value_default = createRule({
|
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
114
|
if ((token.type === "unknown" || token.type === "css-function") && "raw" in token && token.raw && erroredRaws.has(token.raw)) continue;
|
|
115
|
-
if (token.type === "unknown") {
|
|
115
|
+
if (token.type === "unknown" || token.type === "ratio" && !expectation.acceptsRatio) {
|
|
116
116
|
const raw = token.raw;
|
|
117
117
|
if (expectation.acceptsMods === true) {} else if (expectation.acceptsMods === false) context.report({
|
|
118
118
|
node,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-value.js","names":[],"sources":["../../src/rules/valid-value.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport type { ValueParserOptions } from '../parsers/value-parser.js';\nimport { getExpectation } from '../property-expectations.js';\n\nconst SKIP_PROPERTIES = new Set([\n 'recipe',\n 'preset',\n 'transition',\n '@keyframes',\n '@property',\n '@font-face',\n '@counter-style',\n '@function',\n 'content',\n 'animation',\n 'animationName',\n 'gridArea',\n 'gridAreas',\n 'gridColumn',\n 'gridColumnStart',\n 'gridColumnEnd',\n 'gridRow',\n 'gridRowStart',\n 'gridRowEnd',\n 'gridTemplate',\n 'gridTemplateAreas',\n 'gridTemplateColumns',\n 'gridTemplateRows',\n 'listStyle',\n 'willChange',\n]);\n\ntype MessageIds =\n | 'unbalancedParens'\n | 'importantNotAllowed'\n | 'unexpectedMod'\n | 'unexpectedColor'\n | 'invalidMod'\n | 'unknownToken'\n | 'parseError';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-value',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Parse style values through the value parser and validate against per-property expectations',\n },\n messages: {\n unbalancedParens: 'Unbalanced parentheses in value.',\n importantNotAllowed:\n 'Do not use !important in tasty styles. Use state specificity instead.',\n unexpectedMod:\n \"Unrecognized token '{{mod}}' in '{{property}}' value. This may be a typo.\",\n unexpectedColor:\n \"Property '{{property}}' does not accept color tokens, but found '{{color}}'.\",\n invalidMod:\n \"Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n unknownToken:\n \"Unknown token '{{token}}' in '{{property}}' value. Check for a typo.\",\n parseError: '{{message}}',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function getParserOpts(): ValueParserOptions {\n const opts: ValueParserOptions = {};\n if (ctx.config.units === false) {\n opts.skipUnitValidation = true;\n } else if (Array.isArray(ctx.config.units)) {\n opts.knownUnits = new Set(ctx.config.units);\n }\n if (ctx.config.functions === false) {\n opts.skipFuncValidation = true;\n } else if (Array.isArray(ctx.config.functions)) {\n opts.knownFuncs = new Set(ctx.config.functions);\n }\n return opts;\n }\n\n function checkValue(\n value: string,\n property: string | null,\n node: TSESTree.Node,\n ): void {\n if (property && SKIP_PROPERTIES.has(property)) return;\n\n const result = parseValue(value, getParserOpts());\n\n const erroredRaws = new Set<string>();\n\n // Report parser-level errors (bracket balance, unknown units/functions)\n for (const error of result.errors) {\n if (error.message.includes('parenthes')) {\n context.report({ node, messageId: 'unbalancedParens' });\n return;\n }\n context.report({\n node,\n messageId: 'parseError',\n data: { message: error.message },\n });\n if (error.raw) {\n erroredRaws.add(error.raw);\n }\n }\n\n if (!property) return;\n\n const expectation = getExpectation(property);\n\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n // Check !important\n if (token.type === 'important') {\n context.report({ node, messageId: 'importantNotAllowed' });\n continue;\n }\n\n // Check color tokens and keywords in non-color properties\n if (\n !expectation.acceptsColor &&\n (token.type === 'color-token' ||\n token.type === 'color-ref' ||\n token.type === 'color-keyword')\n ) {\n const colorName =\n token.type === 'color-token'\n ? `#${token.name}`\n : token.type === 'color-ref'\n ? `##${token.name}`\n : token.raw;\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: colorName },\n });\n continue;\n }\n\n // Check color functions in non-color properties\n if (\n !expectation.acceptsColor &&\n token.type === 'css-function' &&\n isColorFunction(token.name)\n ) {\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: `${token.name}()` },\n });\n continue;\n }\n\n // Skip tokens already reported via parser errors\n if (\n (token.type === 'unknown' || token.type === 'css-function') &&\n 'raw' in token &&\n token.raw &&\n erroredRaws.has(token.raw)\n ) {\n continue;\n }\n\n // Check unknown tokens against property expectations\n if (token.type === 'unknown') {\n const raw = token.raw;\n\n if (expectation.acceptsMods === true) {\n // Passthrough: accept any unknown token\n } else if (expectation.acceptsMods === false) {\n context.report({\n node,\n messageId: 'unexpectedMod',\n data: { property, mod: raw },\n });\n } else if (Array.isArray(expectation.acceptsMods)) {\n if (!expectation.acceptsMods.includes(raw)) {\n context.report({\n node,\n messageId: 'invalidMod',\n data: {\n property,\n mod: raw,\n accepted: expectation.acceptsMods.join(', '),\n },\n });\n }\n }\n }\n }\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, property, node);\n });\n },\n});\n\nconst COLOR_FUNC_NAMES = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'color-mix',\n 'color-contrast',\n]);\n\nfunction isColorFunction(name: string): boolean {\n return COLOR_FUNC_NAMES.has(name);\n}\n"],"mappings":";;;;;;;AAQA,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAWF,0BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,8FACH;EACD,UAAU;GACR,kBAAkB;GAClB,qBACE;GACF,eACE;GACF,iBACE;GACF,YACE;GACF,cACE;GACF,YAAY;GACb;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,gBAAoC;GAC3C,MAAM,OAA2B,EAAE;AACnC,OAAI,IAAI,OAAO,UAAU,MACvB,MAAK,qBAAqB;YACjB,MAAM,QAAQ,IAAI,OAAO,MAAM,CACxC,MAAK,aAAa,IAAI,IAAI,IAAI,OAAO,MAAM;AAE7C,OAAI,IAAI,OAAO,cAAc,MAC3B,MAAK,qBAAqB;YACjB,MAAM,QAAQ,IAAI,OAAO,UAAU,CAC5C,MAAK,aAAa,IAAI,IAAI,IAAI,OAAO,UAAU;AAEjD,UAAO;;EAGT,SAAS,WACP,OACA,UACA,MACM;AACN,OAAI,YAAY,gBAAgB,IAAI,SAAS,CAAE;GAE/C,MAAM,SAAS,WAAW,OAAO,eAAe,CAAC;GAEjD,MAAM,8BAAc,IAAI,KAAa;AAGrC,QAAK,MAAM,SAAS,OAAO,QAAQ;AACjC,QAAI,MAAM,QAAQ,SAAS,YAAY,EAAE;AACvC,aAAQ,OAAO;MAAE;MAAM,WAAW;MAAoB,CAAC;AACvD;;AAEF,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,SAAS,MAAM,SAAS;KACjC,CAAC;AACF,QAAI,MAAM,IACR,aAAY,IAAI,MAAM,IAAI;;AAI9B,OAAI,CAAC,SAAU;GAEf,MAAM,cAAc,eAAe,SAAS;AAE5C,QAAK,MAAM,SAAS,OAAO,OACzB,MAAK,MAAM,QAAQ,MAAM,MACvB,MAAK,MAAM,SAAS,KAAK,QAAQ;AAE/B,QAAI,MAAM,SAAS,aAAa;AAC9B,aAAQ,OAAO;MAAE;MAAM,WAAW;MAAuB,CAAC;AAC1D;;AAIF,QACE,CAAC,YAAY,iBACZ,MAAM,SAAS,iBACd,MAAM,SAAS,eACf,MAAM,SAAS,kBACjB;KACA,MAAM,YACJ,MAAM,SAAS,gBACX,IAAI,MAAM,SACV,MAAM,SAAS,cACb,KAAK,MAAM,SACX,MAAM;AACd,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE;OAAU,OAAO;OAAW;MACrC,CAAC;AACF;;AAIF,QACE,CAAC,YAAY,gBACb,MAAM,SAAS,kBACf,gBAAgB,MAAM,KAAK,EAC3B;AACA,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE;OAAU,OAAO,GAAG,MAAM,KAAK;OAAK;MAC7C,CAAC;AACF;;AAIF,SACG,MAAM,SAAS,aAAa,MAAM,SAAS,mBAC5C,SAAS,SACT,MAAM,OACN,YAAY,IAAI,MAAM,IAAI,CAE1B;
|
|
1
|
+
{"version":3,"file":"valid-value.js","names":[],"sources":["../../src/rules/valid-value.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport type { ValueParserOptions } from '../parsers/value-parser.js';\nimport { getExpectation } from '../property-expectations.js';\n\nconst SKIP_PROPERTIES = new Set([\n 'recipe',\n 'preset',\n 'transition',\n '@keyframes',\n '@property',\n '@font-face',\n '@counter-style',\n '@function',\n 'content',\n 'animation',\n 'animationName',\n 'gridArea',\n 'gridAreas',\n 'gridColumn',\n 'gridColumnStart',\n 'gridColumnEnd',\n 'gridRow',\n 'gridRowStart',\n 'gridRowEnd',\n 'gridTemplate',\n 'gridTemplateAreas',\n 'gridTemplateColumns',\n 'gridTemplateRows',\n 'listStyle',\n 'willChange',\n]);\n\ntype MessageIds =\n | 'unbalancedParens'\n | 'importantNotAllowed'\n | 'unexpectedMod'\n | 'unexpectedColor'\n | 'invalidMod'\n | 'unknownToken'\n | 'parseError';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-value',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Parse style values through the value parser and validate against per-property expectations',\n },\n messages: {\n unbalancedParens: 'Unbalanced parentheses in value.',\n importantNotAllowed:\n 'Do not use !important in tasty styles. Use state specificity instead.',\n unexpectedMod:\n \"Unrecognized token '{{mod}}' in '{{property}}' value. This may be a typo.\",\n unexpectedColor:\n \"Property '{{property}}' does not accept color tokens, but found '{{color}}'.\",\n invalidMod:\n \"Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n unknownToken:\n \"Unknown token '{{token}}' in '{{property}}' value. Check for a typo.\",\n parseError: '{{message}}',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function getParserOpts(): ValueParserOptions {\n const opts: ValueParserOptions = {};\n if (ctx.config.units === false) {\n opts.skipUnitValidation = true;\n } else if (Array.isArray(ctx.config.units)) {\n opts.knownUnits = new Set(ctx.config.units);\n }\n if (ctx.config.functions === false) {\n opts.skipFuncValidation = true;\n } else if (Array.isArray(ctx.config.functions)) {\n opts.knownFuncs = new Set(ctx.config.functions);\n }\n return opts;\n }\n\n function checkValue(\n value: string,\n property: string | null,\n node: TSESTree.Node,\n ): void {\n if (property && SKIP_PROPERTIES.has(property)) return;\n\n const result = parseValue(value, getParserOpts());\n\n const erroredRaws = new Set<string>();\n\n // Report parser-level errors (bracket balance, unknown units/functions)\n for (const error of result.errors) {\n if (error.message.includes('parenthes')) {\n context.report({ node, messageId: 'unbalancedParens' });\n return;\n }\n context.report({\n node,\n messageId: 'parseError',\n data: { message: error.message },\n });\n if (error.raw) {\n erroredRaws.add(error.raw);\n }\n }\n\n if (!property) return;\n\n const expectation = getExpectation(property);\n\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n // Check !important\n if (token.type === 'important') {\n context.report({ node, messageId: 'importantNotAllowed' });\n continue;\n }\n\n // Check color tokens and keywords in non-color properties\n if (\n !expectation.acceptsColor &&\n (token.type === 'color-token' ||\n token.type === 'color-ref' ||\n token.type === 'color-keyword')\n ) {\n const colorName =\n token.type === 'color-token'\n ? `#${token.name}`\n : token.type === 'color-ref'\n ? `##${token.name}`\n : token.raw;\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: colorName },\n });\n continue;\n }\n\n // Check color functions in non-color properties\n if (\n !expectation.acceptsColor &&\n token.type === 'css-function' &&\n isColorFunction(token.name)\n ) {\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: `${token.name}()` },\n });\n continue;\n }\n\n // Skip tokens already reported via parser errors\n if (\n (token.type === 'unknown' || token.type === 'css-function') &&\n 'raw' in token &&\n token.raw &&\n erroredRaws.has(token.raw)\n ) {\n continue;\n }\n\n // Check unknown tokens against property expectations. A ratio\n // outside `aspectRatio` is as unrecognized as any other word.\n if (\n token.type === 'unknown' ||\n (token.type === 'ratio' && !expectation.acceptsRatio)\n ) {\n const raw = token.raw;\n\n if (expectation.acceptsMods === true) {\n // Passthrough: accept any unknown token\n } else if (expectation.acceptsMods === false) {\n context.report({\n node,\n messageId: 'unexpectedMod',\n data: { property, mod: raw },\n });\n } else if (Array.isArray(expectation.acceptsMods)) {\n if (!expectation.acceptsMods.includes(raw)) {\n context.report({\n node,\n messageId: 'invalidMod',\n data: {\n property,\n mod: raw,\n accepted: expectation.acceptsMods.join(', '),\n },\n });\n }\n }\n }\n }\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, property, node);\n });\n },\n});\n\nconst COLOR_FUNC_NAMES = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'color-mix',\n 'color-contrast',\n]);\n\nfunction isColorFunction(name: string): boolean {\n return COLOR_FUNC_NAMES.has(name);\n}\n"],"mappings":"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"}
|
package/dist/style-values.js
CHANGED
|
@@ -28,7 +28,11 @@ function styleValueListeners(ctx, check, options = {}) {
|
|
|
28
28
|
}
|
|
29
29
|
} else {
|
|
30
30
|
const visible = node.type === "Literal" || node.type === "JSXAttribute" || getStringValue(node) !== null || node.type === "UnaryExpression" && node.argument.type === "Literal";
|
|
31
|
-
if (attribute && visible
|
|
31
|
+
if (attribute && visible) {
|
|
32
|
+
const kind = ctx.jsxPropKind(attribute);
|
|
33
|
+
if (kind === "component") return;
|
|
34
|
+
if (kind === "unknown" && ctx.isExternalJSXElement(attribute.parent)) return;
|
|
35
|
+
}
|
|
32
36
|
check(property, node);
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -59,6 +63,7 @@ function styleValueListeners(ctx, check, options = {}) {
|
|
|
59
63
|
let tag = opening.name;
|
|
60
64
|
while (tag.type === "JSXMemberExpression") tag = tag.object;
|
|
61
65
|
if (tag.type !== "JSXIdentifier" || !/^[A-Z]/.test(tag.name)) return;
|
|
66
|
+
if (!ctx.typeAwareJSX && ctx.isExternalJSXElement(opening)) return;
|
|
62
67
|
if (options.requireTastyJSX && !ctx.isTastyJSXComponent(node, tag.name)) return;
|
|
63
68
|
const value = node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value;
|
|
64
69
|
if (value) visit(key, value, node);
|
package/dist/style-values.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style-values.js","names":[],"sources":["../src/style-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { TastyContext } from './context.js';\nimport { KNOWN_CSS_PROPERTIES, KNOWN_TASTY_PROPERTIES } from './constants.js';\nimport { getKeyName, getStringValue, unwrapExpression } from './utils.js';\n\ninterface ValueListenerOptions {\n /** Collect declarations before checking their values (token existence rules). */\n onStyleObject?: (node: TSESTree.ObjectExpression) => void;\n /** Rewrites that depend on Tasty semantics need evidence beyond the prop name. */\n requireTastyJSX?: boolean;\n /** Deferred checks run after all declarations have been collected. */\n onProgramExit?: () => void;\n}\n\n/** Visit visible values without evaluating bindings, calls, or interpolations. */\nexport function styleValueListeners(\n ctx: TastyContext,\n check: (property: string, node: TSESTree.Node) => void,\n options: ValueListenerOptions = {},\n) {\n function visit(\n property: string,\n node: TSESTree.Node,\n attribute?: TSESTree.JSXAttribute,\n ): void {\n node = unwrapExpression(node);\n if (node.type === 'ObjectExpression') {\n // A property value object is a state map, never a sub-element here.\n for (const prop of node.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const value = unwrapExpression(prop.value);\n if (value.type !== 'ObjectExpression')\n visit(property, value, attribute);\n }\n }\n } else if (node.type === 'ConditionalExpression') {\n visit(property, node.consequent, attribute);\n visit(property, node.alternate, attribute);\n } else if (node.type === 'LogicalExpression') {\n const left = unwrapExpression(node.left);\n const str = getStringValue(left);\n const known = left.type === 'Literal' || str !== null;\n const value = left.type === 'Literal' ? left.value : str;\n if (node.operator === '&&') {\n // The left operand can only be the result when it is falsy.\n if (!known || value) visit(property, node.right, attribute);\n } else if (known) {\n visit(\n property,\n node.operator === '??'\n ? value == null\n ? node.right\n : left\n : value\n ? left\n : node.right,\n attribute,\n );\n } else {\n visit(property, node.left, attribute);\n visit(property, node.right, attribute);\n }\n } else {\n const visible =\n node.type === 'Literal' ||\n node.type === 'JSXAttribute' ||\n getStringValue(node) !== null ||\n (node.type === 'UnaryExpression' && node.argument.type === 'Literal');\n // Opaque values cannot produce a value diagnostic; avoid a compiler pass.\n if (attribute && visible
|
|
1
|
+
{"version":3,"file":"style-values.js","names":[],"sources":["../src/style-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { TastyContext } from './context.js';\nimport { KNOWN_CSS_PROPERTIES, KNOWN_TASTY_PROPERTIES } from './constants.js';\nimport { getKeyName, getStringValue, unwrapExpression } from './utils.js';\n\ninterface ValueListenerOptions {\n /** Collect declarations before checking their values (token existence rules). */\n onStyleObject?: (node: TSESTree.ObjectExpression) => void;\n /** Rewrites that depend on Tasty semantics need evidence beyond the prop name. */\n requireTastyJSX?: boolean;\n /** Deferred checks run after all declarations have been collected. */\n onProgramExit?: () => void;\n}\n\n/** Visit visible values without evaluating bindings, calls, or interpolations. */\nexport function styleValueListeners(\n ctx: TastyContext,\n check: (property: string, node: TSESTree.Node) => void,\n options: ValueListenerOptions = {},\n) {\n function visit(\n property: string,\n node: TSESTree.Node,\n attribute?: TSESTree.JSXAttribute,\n ): void {\n node = unwrapExpression(node);\n if (node.type === 'ObjectExpression') {\n // A property value object is a state map, never a sub-element here.\n for (const prop of node.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const value = unwrapExpression(prop.value);\n if (value.type !== 'ObjectExpression')\n visit(property, value, attribute);\n }\n }\n } else if (node.type === 'ConditionalExpression') {\n visit(property, node.consequent, attribute);\n visit(property, node.alternate, attribute);\n } else if (node.type === 'LogicalExpression') {\n const left = unwrapExpression(node.left);\n const str = getStringValue(left);\n const known = left.type === 'Literal' || str !== null;\n const value = left.type === 'Literal' ? left.value : str;\n if (node.operator === '&&') {\n // The left operand can only be the result when it is falsy.\n if (!known || value) visit(property, node.right, attribute);\n } else if (known) {\n visit(\n property,\n node.operator === '??'\n ? value == null\n ? node.right\n : left\n : value\n ? left\n : node.right,\n attribute,\n );\n } else {\n visit(property, node.left, attribute);\n visit(property, node.right, attribute);\n }\n } else {\n const visible =\n node.type === 'Literal' ||\n node.type === 'JSXAttribute' ||\n getStringValue(node) !== null ||\n (node.type === 'UnaryExpression' && node.argument.type === 'Literal');\n // Opaque values cannot produce a value diagnostic; avoid a compiler pass.\n if (attribute && visible) {\n const kind = ctx.jsxPropKind(attribute);\n if (kind === 'component') return;\n // Where types do not decide, a third-party component owns its props.\n if (kind === 'unknown' && ctx.isExternalJSXElement(attribute.parent))\n return;\n }\n check(property, node);\n }\n }\n\n return {\n 'Program:exit'() {\n ctx.finishJSXAnalysis();\n options.onProgramExit?.();\n },\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n ctx.trackImport(node);\n },\n ObjectExpression(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n options.onStyleObject?.(node);\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key === null || /^[A-Z@&]/.test(key) || key === '$') continue;\n visit(key, prop.value);\n }\n },\n JSXAttribute(node: TSESTree.JSXAttribute) {\n if (node.name.type !== 'JSXIdentifier') return;\n const key = node.name.name;\n if (\n !KNOWN_TASTY_PROPERTIES.has(key) &&\n !KNOWN_CSS_PROPERTIES.has(key) &&\n !ctx.config.styles.includes(key)\n )\n return;\n\n const opening = node.parent;\n if (opening?.type !== 'JSXOpeningElement') return;\n let tag = opening.name;\n while (tag.type === 'JSXMemberExpression') tag = tag.object;\n // Native HTML/SVG attributes contain CSS, not Tasty's value language.\n if (tag.type !== 'JSXIdentifier' || !/^[A-Z]/.test(tag.name)) return;\n // Without prop types, a third-party component is taken to own its props.\n if (!ctx.typeAwareJSX && ctx.isExternalJSXElement(opening)) return;\n if (options.requireTastyJSX && !ctx.isTastyJSXComponent(node, tag.name))\n return;\n\n const value =\n node.value?.type === 'JSXExpressionContainer'\n ? node.value.expression\n : node.value;\n if (value) visit(key, value, node);\n else visit(key, node, node); // A bare JSX attribute means boolean true.\n },\n };\n}\n\n/** Adapt a node listener to the string-only rules. */\nexport function stringStyleValueListeners(\n ctx: TastyContext,\n check: (value: string, node: TSESTree.Node, property: string) => void,\n options: ValueListenerOptions = {},\n) {\n return styleValueListeners(\n ctx,\n (property, node) => {\n const value = getStringValue(node);\n if (value !== null) check(value, node, property);\n },\n options,\n );\n}\n"],"mappings":"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"}
|
package/dist/type-aware-jsx.js
CHANGED
|
@@ -241,9 +241,17 @@ function makeClassifier(T, program, filename, text) {
|
|
|
241
241
|
return kind;
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
|
+
function typeAwareJSXOption(context) {
|
|
245
|
+
return context.settings.tasty?.typeAwareJSX;
|
|
246
|
+
}
|
|
247
|
+
/** Whether `settings.tasty.typeAwareJSX` turns the classifier on. */
|
|
248
|
+
function isTypeAwareJSX(context) {
|
|
249
|
+
const option = typeAwareJSXOption(context);
|
|
250
|
+
return option !== void 0 && option !== false;
|
|
251
|
+
}
|
|
244
252
|
/** No compiler is loaded, or program created, unless the setting is enabled. */
|
|
245
253
|
function jsxPropKind(context, attribute) {
|
|
246
|
-
const option = context
|
|
254
|
+
const option = typeAwareJSXOption(context);
|
|
247
255
|
if (option === void 0 || option === false) return "unknown";
|
|
248
256
|
if (option !== true && (!option || typeof option !== "object" || Array.isArray(option) || option.project !== void 0 && typeof option.project !== "string")) throw new Error("tasty: typeAwareJSX must be true, false, or { project: string }.");
|
|
249
257
|
const filename = resolve(context.cwd, context.filename);
|
|
@@ -276,5 +284,5 @@ function finishJSXAnalysis(context) {
|
|
|
276
284
|
}
|
|
277
285
|
|
|
278
286
|
//#endregion
|
|
279
|
-
export { finishJSXAnalysis, jsxPropKind };
|
|
287
|
+
export { finishJSXAnalysis, isTypeAwareJSX, jsxPropKind };
|
|
280
288
|
//# sourceMappingURL=type-aware-jsx.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type-aware-jsx.js","names":[],"sources":["../src/type-aware-jsx.ts"],"sourcesContent":["import { statSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, resolve } from 'node:path';\nimport type ts from 'typescript';\nimport type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext } from '@typescript-eslint/utils/ts-eslint';\n\n/** Experimental, independently managed TypeScript analysis for either linter. */\nexport interface TypeAwareJSXOptions {\n /** tsconfig path relative to the linter's cwd; otherwise find the nearest one. */\n project?: string;\n}\n\ntype PropKind = 'style' | 'component' | 'unknown';\ntype TypeScript = typeof ts;\ntype Classifier = (attribute: TSESTree.JSXAttribute) => PropKind;\n\ninterface Project {\n config: ts.ParsedCommandLine;\n configVersions: Map<string, string>;\n program?: ts.Program;\n versions: Map<string, string>;\n overlay?: { filename: string; text: string };\n sourceFiles: Map<\n string,\n {\n version: string;\n overlayText?: string;\n parseKey: string;\n source: ts.SourceFile;\n }\n >;\n excludedFiles: Map<string, string>;\n}\n\nconst require = createRequire(import.meta.url);\nlet typescript: TypeScript | undefined;\nconst projects = new Map<string, Project>();\n// All value rules share one classifier for a file, including in Oxlint.\nconst classifiers = new WeakMap<\n object,\n Map<\n string,\n {\n filename: string;\n text: string;\n classify: Classifier;\n }\n >\n>();\n\nfunction getTypeScript(): TypeScript {\n if (!typescript) {\n let module: Partial<TypeScript>;\n try {\n module = require('typescript') as Partial<TypeScript>;\n } catch {\n throw new Error(\n 'tasty: typeAwareJSX requires TypeScript. Install typescript as a dev dependency.',\n );\n }\n // TypeScript 7 (the native port) exports only its version from the main entry.\n if (typeof module.createProgram !== 'function') {\n throw new Error(\n `tasty: typeAwareJSX requires the TypeScript compiler API, which ${\n module.version\n ? `typescript ${module.version}`\n : 'the installed typescript'\n } does not provide. Install TypeScript 5.6–6.x as a dev dependency.`,\n );\n }\n typescript = module as TypeScript;\n }\n return typescript;\n}\n\nfunction version(filename: string): string {\n try {\n const stat = statSync(filename);\n return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;\n } catch {\n return 'missing';\n }\n}\n\nfunction changed(versions: Map<string, string>): boolean {\n for (const [filename, previous] of versions) {\n if (version(filename) !== previous) return true;\n }\n return false;\n}\n\nfunction readConfig(\n T: TypeScript,\n configPath: string,\n configVersions: Map<string, string>,\n): ts.ParsedCommandLine {\n const config = T.getParsedCommandLineOfConfigFile(\n configPath,\n {},\n {\n ...T.sys,\n readFile(filename) {\n configVersions.set(filename, version(filename));\n return T.sys.readFile(filename);\n },\n onUnRecoverableConfigFileDiagnostic(diagnostic) {\n throw new Error(\n `tasty: ${T.flattenDiagnosticMessageText(diagnostic.messageText, '\\n')}`,\n );\n },\n },\n );\n if (!config || config.errors.length) {\n throw new Error(\n `tasty: invalid typeAwareJSX project ${configPath}: ${config?.errors\n .map((d) => T.flattenDiagnosticMessageText(d.messageText, '\\n'))\n .join('\\n')}`,\n );\n }\n return config;\n}\n\nfunction readProject(T: TypeScript, configPath: string): Project {\n const configVersions = new Map<string, string>();\n return {\n config: readConfig(T, configPath, configVersions),\n configVersions,\n versions: new Map(),\n sourceFiles: new Map(),\n excludedFiles: new Map(),\n };\n}\n\nfunction getProgram(\n T: TypeScript,\n configPath: string,\n filename: string,\n text: string,\n): ts.Program {\n let project = projects.get(configPath);\n let rootsChanged = false;\n if (!project || changed(project.configVersions)) {\n project = readProject(T, configPath);\n } else if (\n project.program &&\n !project.program.getSourceFile(filename) &&\n project.excludedFiles.get(filename) !== version(filename)\n ) {\n const refreshed = readProject(T, configPath);\n const previousFiles = project.config.fileNames;\n rootsChanged =\n refreshed.config.fileNames.length !== previousFiles.length ||\n refreshed.config.fileNames.some(\n (file, index) => file !== previousFiles[index],\n );\n if (rootsChanged) project.config = refreshed.config;\n if (!project.config.fileNames.includes(filename))\n project.excludedFiles.set(filename, version(filename));\n else project.excludedFiles.delete(filename);\n }\n // Bound retained compiler graphs in long-lived editor processes.\n projects.delete(configPath);\n projects.set(configPath, project);\n if (projects.size > 3) projects.delete(projects.keys().next().value!);\n\n const sourcesChanged = changed(project.versions);\n const overlay =\n project.excludedFiles.has(filename) &&\n !project.program?.getSourceFile(filename) &&\n !rootsChanged &&\n !sourcesChanged\n ? undefined\n : T.sys.readFile(filename) === text\n ? undefined\n : { filename, text };\n const overlayChanged =\n project.overlay?.filename !== overlay?.filename ||\n project.overlay?.text !== overlay?.text;\n if (!project.program || rootsChanged || overlayChanged || sourcesChanged) {\n const host = T.createCompilerHost(project.config.options, true);\n host.getParsedCommandLine = (path) =>\n readConfig(T, path, project.configVersions);\n const readFile = host.readFile;\n host.readFile = (path) =>\n resolve(path) === overlay?.filename ? overlay.text : readFile(path);\n const getSourceFile = host.getSourceFile;\n host.getSourceFile = (path, options, onError, force) => {\n const key = resolve(path);\n const stamp = version(key);\n const overlayText = key === overlay?.filename ? overlay.text : undefined;\n const parseOptions =\n typeof options === 'number' ? { languageVersion: options } : options;\n const parseKey = [\n parseOptions.languageVersion,\n parseOptions.impliedNodeFormat,\n parseOptions.jsDocParsingMode,\n ].join(':');\n const cached = project.sourceFiles.get(key);\n if (\n !force &&\n cached?.version === stamp &&\n cached.overlayText === overlayText &&\n cached.parseKey === parseKey\n )\n return cached.source;\n const source = getSourceFile(path, options, onError, force);\n if (source)\n project.sourceFiles.set(key, {\n version: stamp,\n overlayText,\n parseKey,\n source,\n });\n else project.sourceFiles.delete(key);\n return source;\n };\n project.program = T.createProgram({\n rootNames: project.config.fileNames,\n options: project.config.options,\n projectReferences: project.config.projectReferences,\n host,\n oldProgram: project.program,\n });\n project.overlay = project.program.getSourceFile(filename)\n ? overlay\n : undefined;\n if (!project.program.getSourceFile(filename))\n project.excludedFiles.set(filename, version(filename));\n project.versions = new Map(\n project.program\n .getSourceFiles()\n .map((file) => [file.fileName, version(file.fileName)]),\n );\n const retained = new Set(\n project.program.getSourceFiles().map((file) => resolve(file.fileName)),\n );\n for (const path of project.sourceFiles.keys()) {\n if (!retained.has(path)) project.sourceFiles.delete(path);\n }\n }\n return project.program;\n}\n\nfunction makeClassifier(\n T: TypeScript,\n program: ts.Program,\n filename: string,\n text: string,\n): Classifier {\n const source = program.getSourceFile(filename);\n // Excluded files and virtual/transformed source keep the existing heuristic.\n if (!source || source.text !== text) return () => 'unknown';\n const checker = program.getTypeChecker();\n const modulePath = T.resolveModuleName(\n '@tenphi/tasty',\n filename,\n program.getCompilerOptions(),\n T.sys,\n ).resolvedModule?.resolvedFileName;\n const moduleFile = modulePath && program.getSourceFile(modulePath);\n const module = moduleFile && checker.getSymbolAtLocation(moduleFile);\n const exports = module ? checker.getExportsOfModule(module) : [];\n const styleTypeSymbols = new Set(\n exports\n ?.filter((s) =>\n ['Styles', 'StyleValue', 'StyleValueStateMap'].includes(s.name),\n )\n .map((s) =>\n s.flags & T.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s,\n ),\n );\n let symbol = exports?.find((s) => s.name === 'Styles');\n if (symbol && symbol.flags & T.SymbolFlags.Alias) {\n symbol = checker.getAliasedSymbol(symbol);\n }\n const styles = symbol && checker.getDeclaredTypeOfSymbol(symbol);\n if (!styles || styles.flags & (T.TypeFlags.Any | T.TypeFlags.Unknown)) {\n return () => 'unknown';\n }\n const styleDeclarations = new Set(\n styles.getProperties().flatMap((property) => property.declarations ?? []),\n );\n // A dependency may resolve a different copy of either styling package.\n function isStyleDeclaration(declaration: ts.Declaration): boolean {\n return (\n styleDeclarations.has(declaration) ||\n /[/\\\\]node_modules[/\\\\](?:@tenphi[/\\\\]tasty|csstype)[/\\\\]/.test(\n declaration.getSourceFile().fileName,\n )\n );\n }\n const attributes = new Map<number, ts.JsxAttribute>();\n function collect(node: ts.Node): void {\n if (T.isJsxAttribute(node)) attributes.set(node.getStart(source), node);\n T.forEachChild(node, collect);\n }\n collect(source);\n\n function referencesStyles(\n node: ts.Node,\n visited = new Set<ts.Node>(),\n bindings = new Map<ts.Symbol, ts.TypeNode>(),\n ): boolean {\n if (visited.has(node)) return false;\n visited.add(node);\n try {\n if (\n T.isPropertySignature(node) ||\n T.isPropertyDeclaration(node) ||\n T.isParameter(node) ||\n T.isTypeAliasDeclaration(node) ||\n T.isMappedTypeNode(node) ||\n T.isParenthesizedTypeNode(node)\n )\n return node.type\n ? referencesStyles(node.type, visited, bindings)\n : false;\n if (T.isIndexedAccessTypeNode(node)) {\n const object = checker.getTypeFromTypeNode(node.objectType);\n const index = checker.getTypeFromTypeNode(node.indexType);\n const keys = index.isUnion() ? index.types : [index];\n // Trace the selected property, not unrelated style props on the object.\n return (\n object === styles ||\n keys.some((key) => {\n if (!key.isStringLiteral() && !key.isNumberLiteral()) return false;\n const property = checker.getPropertyOfType(\n object,\n String(key.value),\n );\n return property?.declarations?.some(\n (declaration) =>\n isStyleDeclaration(declaration) ||\n referencesStyles(declaration, visited, bindings),\n );\n })\n );\n }\n if (T.isTypeReferenceNode(node) || T.isImportTypeNode(node)) {\n const name = T.isTypeReferenceNode(node)\n ? node.typeName\n : node.qualifier;\n let reference = name && checker.getSymbolAtLocation(name);\n if (reference && reference.flags & T.SymbolFlags.Alias) {\n reference = checker.getAliasedSymbol(reference);\n }\n if (reference && styleTypeSymbols.has(reference)) return true;\n if (reference?.declarations?.some(isStyleDeclaration)) return true;\n const bound = reference && bindings.get(reference);\n if (bound) return referencesStyles(bound, visited, bindings);\n if (\n reference?.declarations?.some((declaration) => {\n if (!T.isTypeAliasDeclaration(declaration)) return false;\n const local = new Map(bindings);\n declaration.typeParameters?.forEach((parameter, index) => {\n const symbol = checker.getSymbolAtLocation(parameter.name);\n const argument = node.typeArguments?.[index] ?? parameter.default;\n if (symbol && argument) local.set(symbol, argument);\n });\n return referencesStyles(declaration.type, visited, local);\n })\n )\n return true;\n }\n if (T.isConditionalTypeNode(node)) {\n // Extract/Exclude-style filters preserve an input value or return never.\n const parameter = (branch: ts.TypeNode) => {\n if (!T.isTypeReferenceNode(branch)) return undefined;\n const symbol = checker.getSymbolAtLocation(branch.typeName);\n return symbol && symbol.flags & T.SymbolFlags.TypeParameter\n ? symbol\n : undefined;\n };\n const trueParameter = parameter(node.trueType);\n const falseParameter = parameter(node.falseType);\n const preservesValue =\n (trueParameter &&\n node.falseType.kind === T.SyntaxKind.NeverKeyword) ||\n (falseParameter &&\n node.trueType.kind === T.SyntaxKind.NeverKeyword) ||\n (trueParameter && trueParameter === falseParameter);\n return (\n Boolean(preservesValue) &&\n (referencesStyles(node.trueType, visited, bindings) ||\n referencesStyles(node.falseType, visited, bindings))\n );\n }\n // Nested fields, function arguments and conditional constraints are content,\n // not evidence that the prop's value itself originates in a style type.\n return (\n (T.isUnionTypeNode(node) || T.isIntersectionTypeNode(node)) &&\n node.types.some((part) => referencesStyles(part, visited, bindings))\n );\n } finally {\n visited.delete(node);\n }\n }\n\n function unresolved(type: ts.Type): boolean {\n if (\n type.flags &\n (T.TypeFlags.Any |\n T.TypeFlags.Unknown |\n T.TypeFlags.TypeParameter |\n T.TypeFlags.Conditional |\n T.TypeFlags.IndexedAccess |\n T.TypeFlags.Never)\n )\n return true;\n return type.isUnionOrIntersection() && type.types.some(unresolved);\n }\n\n function hasPrimitiveFlag(type: ts.Type, flags: number): boolean {\n return (\n Boolean(type.flags & flags) ||\n (type.isIntersection() &&\n type.types.some((part) => hasPrimitiveFlag(part, flags)))\n );\n }\n\n function unresolvedComponentProp(\n node: ts.JsxAttribute,\n name: string,\n ): boolean {\n const tag = node.parent.parent.tagName;\n const component = checker.getTypeAtLocation(tag);\n return [\n ...component.getCallSignatures(),\n ...component.getConstructSignatures(),\n ].some((signature) => {\n const parameter = signature.parameters[0];\n if (!parameter) return false;\n const props = checker.getTypeOfSymbolAtLocation(parameter, tag);\n const property = checker.getPropertyOfType(props, name);\n return property\n ? unresolved(checker.getTypeOfSymbolAtLocation(property, tag))\n : false;\n });\n }\n\n const results = new Map<number, PropKind>();\n return (attribute) => {\n const start = attribute.range[0];\n const cached = results.get(start);\n if (cached) return cached;\n const node = attributes.get(start);\n let kind: PropKind = 'unknown';\n if (node && node.end === attribute.range[1] && T.isIdentifier(node.name)) {\n const props = checker.getContextualType(node.parent);\n const property =\n props && checker.getPropertyOfType(props, node.name.text);\n const declarations = property?.declarations;\n if (property && declarations?.length) {\n const type = checker.getTypeOfSymbolAtLocation(property, node);\n if (\n declarations.some((d) => isStyleDeclaration(d) || referencesStyles(d))\n ) {\n kind = 'style';\n } else if (\n !unresolved(type) &&\n !(\n declarations.some((declaration) =>\n unresolved(checker.getTypeAtLocation(declaration)),\n ) && unresolvedComponentProp(node, node.name.text)\n )\n ) {\n // Broad primitives may be hand-written style props. Preserve checks.\n const nonNullable = checker.getNonNullableType(type);\n const parts = nonNullable.isUnion()\n ? nonNullable.types\n : [nonNullable];\n const primitive =\n T.TypeFlags.StringLike |\n T.TypeFlags.NumberLike |\n T.TypeFlags.BooleanLike |\n T.TypeFlags.BigIntLike;\n const broad =\n T.TypeFlags.String |\n T.TypeFlags.Number |\n T.TypeFlags.Boolean |\n T.TypeFlags.BigInt;\n const broadPrimitive =\n parts.every((part) => hasPrimitiveFlag(part, primitive)) &&\n (Boolean(nonNullable.flags & broad) ||\n parts.some((part) => hasPrimitiveFlag(part, broad)) ||\n parts.every((part) =>\n hasPrimitiveFlag(part, T.TypeFlags.BooleanLike),\n ));\n if (!broadPrimitive) kind = 'component';\n }\n }\n }\n results.set(start, kind);\n return kind;\n };\n}\n\n/** No compiler is loaded, or program created, unless the setting is enabled. */\nexport function jsxPropKind(\n context: RuleContext<string, unknown[]>,\n attribute: TSESTree.JSXAttribute,\n): PropKind {\n const settings = context.settings.tasty as\n | { typeAwareJSX?: boolean | TypeAwareJSXOptions }\n | undefined;\n const option = settings?.typeAwareJSX;\n if (option === undefined || option === false) return 'unknown';\n if (\n option !== true &&\n (!option ||\n typeof option !== 'object' ||\n Array.isArray(option) ||\n (option.project !== undefined && typeof option.project !== 'string'))\n )\n throw new Error(\n 'tasty: typeAwareJSX must be true, false, or { project: string }.',\n );\n\n const filename = resolve(context.cwd, context.filename);\n const explicit = option === true ? undefined : option.project;\n const sourceCode = context.sourceCode;\n const key = explicit ? resolve(context.cwd, explicit) : 'nearest';\n let byProject = classifiers.get(sourceCode);\n if (!byProject) {\n byProject = new Map();\n classifiers.set(sourceCode, byProject);\n }\n let cached = byProject.get(key);\n if (\n !cached ||\n cached.filename !== filename ||\n cached.text !== sourceCode.text\n ) {\n const T = getTypeScript();\n const configPath = explicit\n ? key\n : T.findConfigFile(dirname(filename), T.sys.fileExists);\n if (!configPath)\n throw new Error(\n `tasty: typeAwareJSX could not find a tsconfig for ${filename}.`,\n );\n const program = getProgram(T, configPath, filename, sourceCode.text);\n cached = {\n filename,\n text: sourceCode.text,\n classify: makeClassifier(T, program, filename, sourceCode.text),\n };\n byProject.set(key, cached);\n }\n return cached.classify(attribute);\n}\n\n/** A SourceCode object may be reused by ESLint after dependencies change. */\nexport function finishJSXAnalysis(\n context: RuleContext<string, unknown[]>,\n): void {\n classifiers.delete(context.sourceCode);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"type-aware-jsx.js","names":[],"sources":["../src/type-aware-jsx.ts"],"sourcesContent":["import { statSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, resolve } from 'node:path';\nimport type ts from 'typescript';\nimport type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext } from '@typescript-eslint/utils/ts-eslint';\n\n/** Experimental, independently managed TypeScript analysis for either linter. */\nexport interface TypeAwareJSXOptions {\n /** tsconfig path relative to the linter's cwd; otherwise find the nearest one. */\n project?: string;\n}\n\nexport type PropKind = 'style' | 'component' | 'unknown';\ntype TypeScript = typeof ts;\ntype Classifier = (attribute: TSESTree.JSXAttribute) => PropKind;\n\ninterface Project {\n config: ts.ParsedCommandLine;\n configVersions: Map<string, string>;\n program?: ts.Program;\n versions: Map<string, string>;\n overlay?: { filename: string; text: string };\n sourceFiles: Map<\n string,\n {\n version: string;\n overlayText?: string;\n parseKey: string;\n source: ts.SourceFile;\n }\n >;\n excludedFiles: Map<string, string>;\n}\n\nconst require = createRequire(import.meta.url);\nlet typescript: TypeScript | undefined;\nconst projects = new Map<string, Project>();\n// All value rules share one classifier for a file, including in Oxlint.\nconst classifiers = new WeakMap<\n object,\n Map<\n string,\n {\n filename: string;\n text: string;\n classify: Classifier;\n }\n >\n>();\n\nfunction getTypeScript(): TypeScript {\n if (!typescript) {\n let module: Partial<TypeScript>;\n try {\n module = require('typescript') as Partial<TypeScript>;\n } catch {\n throw new Error(\n 'tasty: typeAwareJSX requires TypeScript. Install typescript as a dev dependency.',\n );\n }\n // TypeScript 7 (the native port) exports only its version from the main entry.\n if (typeof module.createProgram !== 'function') {\n throw new Error(\n `tasty: typeAwareJSX requires the TypeScript compiler API, which ${\n module.version\n ? `typescript ${module.version}`\n : 'the installed typescript'\n } does not provide. Install TypeScript 5.6–6.x as a dev dependency.`,\n );\n }\n typescript = module as TypeScript;\n }\n return typescript;\n}\n\nfunction version(filename: string): string {\n try {\n const stat = statSync(filename);\n return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;\n } catch {\n return 'missing';\n }\n}\n\nfunction changed(versions: Map<string, string>): boolean {\n for (const [filename, previous] of versions) {\n if (version(filename) !== previous) return true;\n }\n return false;\n}\n\nfunction readConfig(\n T: TypeScript,\n configPath: string,\n configVersions: Map<string, string>,\n): ts.ParsedCommandLine {\n const config = T.getParsedCommandLineOfConfigFile(\n configPath,\n {},\n {\n ...T.sys,\n readFile(filename) {\n configVersions.set(filename, version(filename));\n return T.sys.readFile(filename);\n },\n onUnRecoverableConfigFileDiagnostic(diagnostic) {\n throw new Error(\n `tasty: ${T.flattenDiagnosticMessageText(diagnostic.messageText, '\\n')}`,\n );\n },\n },\n );\n if (!config || config.errors.length) {\n throw new Error(\n `tasty: invalid typeAwareJSX project ${configPath}: ${config?.errors\n .map((d) => T.flattenDiagnosticMessageText(d.messageText, '\\n'))\n .join('\\n')}`,\n );\n }\n return config;\n}\n\nfunction readProject(T: TypeScript, configPath: string): Project {\n const configVersions = new Map<string, string>();\n return {\n config: readConfig(T, configPath, configVersions),\n configVersions,\n versions: new Map(),\n sourceFiles: new Map(),\n excludedFiles: new Map(),\n };\n}\n\nfunction getProgram(\n T: TypeScript,\n configPath: string,\n filename: string,\n text: string,\n): ts.Program {\n let project = projects.get(configPath);\n let rootsChanged = false;\n if (!project || changed(project.configVersions)) {\n project = readProject(T, configPath);\n } else if (\n project.program &&\n !project.program.getSourceFile(filename) &&\n project.excludedFiles.get(filename) !== version(filename)\n ) {\n const refreshed = readProject(T, configPath);\n const previousFiles = project.config.fileNames;\n rootsChanged =\n refreshed.config.fileNames.length !== previousFiles.length ||\n refreshed.config.fileNames.some(\n (file, index) => file !== previousFiles[index],\n );\n if (rootsChanged) project.config = refreshed.config;\n if (!project.config.fileNames.includes(filename))\n project.excludedFiles.set(filename, version(filename));\n else project.excludedFiles.delete(filename);\n }\n // Bound retained compiler graphs in long-lived editor processes.\n projects.delete(configPath);\n projects.set(configPath, project);\n if (projects.size > 3) projects.delete(projects.keys().next().value!);\n\n const sourcesChanged = changed(project.versions);\n const overlay =\n project.excludedFiles.has(filename) &&\n !project.program?.getSourceFile(filename) &&\n !rootsChanged &&\n !sourcesChanged\n ? undefined\n : T.sys.readFile(filename) === text\n ? undefined\n : { filename, text };\n const overlayChanged =\n project.overlay?.filename !== overlay?.filename ||\n project.overlay?.text !== overlay?.text;\n if (!project.program || rootsChanged || overlayChanged || sourcesChanged) {\n const host = T.createCompilerHost(project.config.options, true);\n host.getParsedCommandLine = (path) =>\n readConfig(T, path, project.configVersions);\n const readFile = host.readFile;\n host.readFile = (path) =>\n resolve(path) === overlay?.filename ? overlay.text : readFile(path);\n const getSourceFile = host.getSourceFile;\n host.getSourceFile = (path, options, onError, force) => {\n const key = resolve(path);\n const stamp = version(key);\n const overlayText = key === overlay?.filename ? overlay.text : undefined;\n const parseOptions =\n typeof options === 'number' ? { languageVersion: options } : options;\n const parseKey = [\n parseOptions.languageVersion,\n parseOptions.impliedNodeFormat,\n parseOptions.jsDocParsingMode,\n ].join(':');\n const cached = project.sourceFiles.get(key);\n if (\n !force &&\n cached?.version === stamp &&\n cached.overlayText === overlayText &&\n cached.parseKey === parseKey\n )\n return cached.source;\n const source = getSourceFile(path, options, onError, force);\n if (source)\n project.sourceFiles.set(key, {\n version: stamp,\n overlayText,\n parseKey,\n source,\n });\n else project.sourceFiles.delete(key);\n return source;\n };\n project.program = T.createProgram({\n rootNames: project.config.fileNames,\n options: project.config.options,\n projectReferences: project.config.projectReferences,\n host,\n oldProgram: project.program,\n });\n project.overlay = project.program.getSourceFile(filename)\n ? overlay\n : undefined;\n if (!project.program.getSourceFile(filename))\n project.excludedFiles.set(filename, version(filename));\n project.versions = new Map(\n project.program\n .getSourceFiles()\n .map((file) => [file.fileName, version(file.fileName)]),\n );\n const retained = new Set(\n project.program.getSourceFiles().map((file) => resolve(file.fileName)),\n );\n for (const path of project.sourceFiles.keys()) {\n if (!retained.has(path)) project.sourceFiles.delete(path);\n }\n }\n return project.program;\n}\n\nfunction makeClassifier(\n T: TypeScript,\n program: ts.Program,\n filename: string,\n text: string,\n): Classifier {\n const source = program.getSourceFile(filename);\n // Excluded files and virtual/transformed source keep the existing heuristic.\n if (!source || source.text !== text) return () => 'unknown';\n const checker = program.getTypeChecker();\n const modulePath = T.resolveModuleName(\n '@tenphi/tasty',\n filename,\n program.getCompilerOptions(),\n T.sys,\n ).resolvedModule?.resolvedFileName;\n const moduleFile = modulePath && program.getSourceFile(modulePath);\n const module = moduleFile && checker.getSymbolAtLocation(moduleFile);\n const exports = module ? checker.getExportsOfModule(module) : [];\n const styleTypeSymbols = new Set(\n exports\n ?.filter((s) =>\n ['Styles', 'StyleValue', 'StyleValueStateMap'].includes(s.name),\n )\n .map((s) =>\n s.flags & T.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s,\n ),\n );\n let symbol = exports?.find((s) => s.name === 'Styles');\n if (symbol && symbol.flags & T.SymbolFlags.Alias) {\n symbol = checker.getAliasedSymbol(symbol);\n }\n const styles = symbol && checker.getDeclaredTypeOfSymbol(symbol);\n if (!styles || styles.flags & (T.TypeFlags.Any | T.TypeFlags.Unknown)) {\n return () => 'unknown';\n }\n const styleDeclarations = new Set(\n styles.getProperties().flatMap((property) => property.declarations ?? []),\n );\n // A dependency may resolve a different copy of either styling package.\n function isStyleDeclaration(declaration: ts.Declaration): boolean {\n return (\n styleDeclarations.has(declaration) ||\n /[/\\\\]node_modules[/\\\\](?:@tenphi[/\\\\]tasty|csstype)[/\\\\]/.test(\n declaration.getSourceFile().fileName,\n )\n );\n }\n const attributes = new Map<number, ts.JsxAttribute>();\n function collect(node: ts.Node): void {\n if (T.isJsxAttribute(node)) attributes.set(node.getStart(source), node);\n T.forEachChild(node, collect);\n }\n collect(source);\n\n function referencesStyles(\n node: ts.Node,\n visited = new Set<ts.Node>(),\n bindings = new Map<ts.Symbol, ts.TypeNode>(),\n ): boolean {\n if (visited.has(node)) return false;\n visited.add(node);\n try {\n if (\n T.isPropertySignature(node) ||\n T.isPropertyDeclaration(node) ||\n T.isParameter(node) ||\n T.isTypeAliasDeclaration(node) ||\n T.isMappedTypeNode(node) ||\n T.isParenthesizedTypeNode(node)\n )\n return node.type\n ? referencesStyles(node.type, visited, bindings)\n : false;\n if (T.isIndexedAccessTypeNode(node)) {\n const object = checker.getTypeFromTypeNode(node.objectType);\n const index = checker.getTypeFromTypeNode(node.indexType);\n const keys = index.isUnion() ? index.types : [index];\n // Trace the selected property, not unrelated style props on the object.\n return (\n object === styles ||\n keys.some((key) => {\n if (!key.isStringLiteral() && !key.isNumberLiteral()) return false;\n const property = checker.getPropertyOfType(\n object,\n String(key.value),\n );\n return property?.declarations?.some(\n (declaration) =>\n isStyleDeclaration(declaration) ||\n referencesStyles(declaration, visited, bindings),\n );\n })\n );\n }\n if (T.isTypeReferenceNode(node) || T.isImportTypeNode(node)) {\n const name = T.isTypeReferenceNode(node)\n ? node.typeName\n : node.qualifier;\n let reference = name && checker.getSymbolAtLocation(name);\n if (reference && reference.flags & T.SymbolFlags.Alias) {\n reference = checker.getAliasedSymbol(reference);\n }\n if (reference && styleTypeSymbols.has(reference)) return true;\n if (reference?.declarations?.some(isStyleDeclaration)) return true;\n const bound = reference && bindings.get(reference);\n if (bound) return referencesStyles(bound, visited, bindings);\n if (\n reference?.declarations?.some((declaration) => {\n if (!T.isTypeAliasDeclaration(declaration)) return false;\n const local = new Map(bindings);\n declaration.typeParameters?.forEach((parameter, index) => {\n const symbol = checker.getSymbolAtLocation(parameter.name);\n const argument = node.typeArguments?.[index] ?? parameter.default;\n if (symbol && argument) local.set(symbol, argument);\n });\n return referencesStyles(declaration.type, visited, local);\n })\n )\n return true;\n }\n if (T.isConditionalTypeNode(node)) {\n // Extract/Exclude-style filters preserve an input value or return never.\n const parameter = (branch: ts.TypeNode) => {\n if (!T.isTypeReferenceNode(branch)) return undefined;\n const symbol = checker.getSymbolAtLocation(branch.typeName);\n return symbol && symbol.flags & T.SymbolFlags.TypeParameter\n ? symbol\n : undefined;\n };\n const trueParameter = parameter(node.trueType);\n const falseParameter = parameter(node.falseType);\n const preservesValue =\n (trueParameter &&\n node.falseType.kind === T.SyntaxKind.NeverKeyword) ||\n (falseParameter &&\n node.trueType.kind === T.SyntaxKind.NeverKeyword) ||\n (trueParameter && trueParameter === falseParameter);\n return (\n Boolean(preservesValue) &&\n (referencesStyles(node.trueType, visited, bindings) ||\n referencesStyles(node.falseType, visited, bindings))\n );\n }\n // Nested fields, function arguments and conditional constraints are content,\n // not evidence that the prop's value itself originates in a style type.\n return (\n (T.isUnionTypeNode(node) || T.isIntersectionTypeNode(node)) &&\n node.types.some((part) => referencesStyles(part, visited, bindings))\n );\n } finally {\n visited.delete(node);\n }\n }\n\n function unresolved(type: ts.Type): boolean {\n if (\n type.flags &\n (T.TypeFlags.Any |\n T.TypeFlags.Unknown |\n T.TypeFlags.TypeParameter |\n T.TypeFlags.Conditional |\n T.TypeFlags.IndexedAccess |\n T.TypeFlags.Never)\n )\n return true;\n return type.isUnionOrIntersection() && type.types.some(unresolved);\n }\n\n function hasPrimitiveFlag(type: ts.Type, flags: number): boolean {\n return (\n Boolean(type.flags & flags) ||\n (type.isIntersection() &&\n type.types.some((part) => hasPrimitiveFlag(part, flags)))\n );\n }\n\n function unresolvedComponentProp(\n node: ts.JsxAttribute,\n name: string,\n ): boolean {\n const tag = node.parent.parent.tagName;\n const component = checker.getTypeAtLocation(tag);\n return [\n ...component.getCallSignatures(),\n ...component.getConstructSignatures(),\n ].some((signature) => {\n const parameter = signature.parameters[0];\n if (!parameter) return false;\n const props = checker.getTypeOfSymbolAtLocation(parameter, tag);\n const property = checker.getPropertyOfType(props, name);\n return property\n ? unresolved(checker.getTypeOfSymbolAtLocation(property, tag))\n : false;\n });\n }\n\n const results = new Map<number, PropKind>();\n return (attribute) => {\n const start = attribute.range[0];\n const cached = results.get(start);\n if (cached) return cached;\n const node = attributes.get(start);\n let kind: PropKind = 'unknown';\n if (node && node.end === attribute.range[1] && T.isIdentifier(node.name)) {\n const props = checker.getContextualType(node.parent);\n const property =\n props && checker.getPropertyOfType(props, node.name.text);\n const declarations = property?.declarations;\n if (property && declarations?.length) {\n const type = checker.getTypeOfSymbolAtLocation(property, node);\n if (\n declarations.some((d) => isStyleDeclaration(d) || referencesStyles(d))\n ) {\n kind = 'style';\n } else if (\n !unresolved(type) &&\n !(\n declarations.some((declaration) =>\n unresolved(checker.getTypeAtLocation(declaration)),\n ) && unresolvedComponentProp(node, node.name.text)\n )\n ) {\n // Broad primitives may be hand-written style props. Preserve checks.\n const nonNullable = checker.getNonNullableType(type);\n const parts = nonNullable.isUnion()\n ? nonNullable.types\n : [nonNullable];\n const primitive =\n T.TypeFlags.StringLike |\n T.TypeFlags.NumberLike |\n T.TypeFlags.BooleanLike |\n T.TypeFlags.BigIntLike;\n const broad =\n T.TypeFlags.String |\n T.TypeFlags.Number |\n T.TypeFlags.Boolean |\n T.TypeFlags.BigInt;\n const broadPrimitive =\n parts.every((part) => hasPrimitiveFlag(part, primitive)) &&\n (Boolean(nonNullable.flags & broad) ||\n parts.some((part) => hasPrimitiveFlag(part, broad)) ||\n parts.every((part) =>\n hasPrimitiveFlag(part, T.TypeFlags.BooleanLike),\n ));\n if (!broadPrimitive) kind = 'component';\n }\n }\n }\n results.set(start, kind);\n return kind;\n };\n}\n\nfunction typeAwareJSXOption(\n context: RuleContext<string, unknown[]>,\n): boolean | TypeAwareJSXOptions | undefined {\n const settings = context.settings.tasty as\n | { typeAwareJSX?: boolean | TypeAwareJSXOptions }\n | undefined;\n return settings?.typeAwareJSX;\n}\n\n/** Whether `settings.tasty.typeAwareJSX` turns the classifier on. */\nexport function isTypeAwareJSX(\n context: RuleContext<string, unknown[]>,\n): boolean {\n const option = typeAwareJSXOption(context);\n return option !== undefined && option !== false;\n}\n\n/** No compiler is loaded, or program created, unless the setting is enabled. */\nexport function jsxPropKind(\n context: RuleContext<string, unknown[]>,\n attribute: TSESTree.JSXAttribute,\n): PropKind {\n const option = typeAwareJSXOption(context);\n if (option === undefined || option === false) return 'unknown';\n if (\n option !== true &&\n (!option ||\n typeof option !== 'object' ||\n Array.isArray(option) ||\n (option.project !== undefined && typeof option.project !== 'string'))\n )\n throw new Error(\n 'tasty: typeAwareJSX must be true, false, or { project: string }.',\n );\n\n const filename = resolve(context.cwd, context.filename);\n const explicit = option === true ? undefined : option.project;\n const sourceCode = context.sourceCode;\n const key = explicit ? resolve(context.cwd, explicit) : 'nearest';\n let byProject = classifiers.get(sourceCode);\n if (!byProject) {\n byProject = new Map();\n classifiers.set(sourceCode, byProject);\n }\n let cached = byProject.get(key);\n if (\n !cached ||\n cached.filename !== filename ||\n cached.text !== sourceCode.text\n ) {\n const T = getTypeScript();\n const configPath = explicit\n ? key\n : T.findConfigFile(dirname(filename), T.sys.fileExists);\n if (!configPath)\n throw new Error(\n `tasty: typeAwareJSX could not find a tsconfig for ${filename}.`,\n );\n const program = getProgram(T, configPath, filename, sourceCode.text);\n cached = {\n filename,\n text: sourceCode.text,\n classify: makeClassifier(T, program, filename, sourceCode.text),\n };\n byProject.set(key, cached);\n }\n return cached.classify(attribute);\n}\n\n/** A SourceCode object may be reused by ESLint after dependencies change. */\nexport function finishJSXAnalysis(\n context: RuleContext<string, unknown[]>,\n): void {\n classifiers.delete(context.sourceCode);\n}\n"],"mappings":"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"}
|
package/dist/utils.js
CHANGED
|
@@ -8,6 +8,15 @@ function unwrapExpression(node) {
|
|
|
8
8
|
return current;
|
|
9
9
|
}
|
|
10
10
|
/** A whole-string pattern in which `*` matches any run of characters. */
|
|
11
|
+
/**
|
|
12
|
+
* A variable's one value definition. TypeScript declares a same-named type or
|
|
13
|
+
* interface (`const Box = tasty(…); type Box = …`) on the same variable, so
|
|
14
|
+
* type definitions are set aside; null for a redeclared or type-only name.
|
|
15
|
+
*/
|
|
16
|
+
function valueDefinition(variable) {
|
|
17
|
+
const defs = variable.defs.filter((def) => def.type !== "Type");
|
|
18
|
+
return defs.length === 1 ? defs[0] : null;
|
|
19
|
+
}
|
|
11
20
|
function wildcardPattern(pattern) {
|
|
12
21
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
22
|
return new RegExp(`^${escaped.replace(/\\\*/g, ".*")}$`);
|
|
@@ -128,5 +137,5 @@ function collectLocalStateAliases(node) {
|
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
//#endregion
|
|
131
|
-
export { collectLocalStateAliases, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, unwrapExpression, validateColorTokenSyntax, wildcardPattern };
|
|
140
|
+
export { collectLocalStateAliases, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, unwrapExpression, validateColorTokenSyntax, valueDefinition, wildcardPattern };
|
|
132
141
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n BUILT_IN_STATE_PREFIXES,\n} from './constants.js';\nimport type { ResolvedConfig } from './types.js';\n\n/** Strip TypeScript wrappers that do not change an expression's runtime value. */\nexport function unwrapExpression(node: TSESTree.Node): TSESTree.Node {\n let current = node;\n\n while (\n current.type === 'TSAsExpression' ||\n current.type === 'TSSatisfiesExpression' ||\n current.type === 'TSNonNullExpression' ||\n current.type === 'TSTypeAssertion' ||\n current.type === 'TSInstantiationExpression'\n ) {\n current = current.expression;\n }\n\n return current;\n}\n\n/** A whole-string pattern in which `*` matches any run of characters. */\nexport function wildcardPattern(pattern: string): RegExp {\n const escaped = pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, '.*')}$`);\n}\n\n/**\n * Gets the string value of a property key node.\n */\nexport function getKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n if (key.type === 'Literal' && typeof key.value === 'number')\n return String(key.value);\n return null;\n}\n\n/**\n * Gets the string value of a node if it is a string literal.\n */\nexport function getStringValue(node: TSESTree.Node): string | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return node.value;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return node.quasis[0].value.cooked ?? null;\n }\n return null;\n}\n\n/**\n * Checks if a value node is a static literal.\n */\nexport function isStaticValue(node: TSESTree.Node): boolean {\n node = unwrapExpression(node);\n if (node.type === 'Literal') return true;\n if (\n node.type === 'UnaryExpression' &&\n node.operator === '-' &&\n node.argument.type === 'Literal'\n ) {\n return true;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return true;\n }\n if (node.type === 'ArrayExpression') {\n return node.elements.every((el) => el !== null && isStaticValue(el));\n }\n if (node.type === 'ObjectExpression') {\n return node.properties.every(\n (prop) =>\n prop.type === 'Property' && !prop.computed && isStaticValue(prop.value),\n );\n }\n return false;\n}\n\n/**\n * Validates color token syntax.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateColorTokenSyntax(token: string): string | null {\n // Strip leading # or ##\n let name = token;\n if (name.startsWith('##')) {\n name = name.slice(2);\n } else if (name.startsWith('#')) {\n name = name.slice(1);\n } else {\n return 'Color token must start with #';\n }\n\n if (name.length === 0) return 'Empty color token name';\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) return 'Empty color token name before opacity';\n\n if (opacitySuffix.startsWith('$')) {\n // Dynamic opacity from CSS custom property — always valid\n return null;\n }\n\n if (opacitySuffix.length === 0) return 'Trailing dot with no opacity value';\n\n // Tasty reads the digits as a decimal fraction (`.5` and `.50` are 50%,\n // `.150` is 15%), so any digit string is in range.\n if (!/^\\d+$/.test(opacitySuffix)) {\n return /^-\\d+$/.test(opacitySuffix)\n ? 'Opacity cannot be negative'\n : `Invalid opacity value '${opacitySuffix}'`;\n }\n }\n\n return null;\n}\n\n/**\n * Checks if a string looks like a raw hex color (not a token).\n * Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).\n */\nexport function isRawHexColor(value: string): boolean {\n if (!value.startsWith('#')) return false;\n const hex = value.slice(1).split('.')[0];\n if (![3, 4, 6, 8].includes(hex.length)) return false;\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\n/**\n * Extracts custom unit from a value token like \"2x\", \"1.5r\", \"3cols\".\n * Returns the unit name, or null if not a custom-unit value.\n */\nexport function extractCustomUnit(token: string): string | null {\n const match = token.match(/^-?[\\d.]+([a-zA-Z]+)$/);\n if (!match) return null;\n return match[1];\n}\n\n/**\n * Checks if a unit is valid (built-in, CSS, or in config).\n */\nexport function isValidUnit(unit: string, config: ResolvedConfig): boolean {\n if (config.units === false) return true;\n if (BUILT_IN_UNITS.has(unit)) return true;\n if (CSS_UNITS.has(unit)) return true;\n if (Array.isArray(config.units) && config.units.includes(unit)) return true;\n return false;\n}\n\n/**\n * Checks if a state alias key (starting with @) is known.\n */\nexport function isKnownStateAlias(\n key: string,\n config: ResolvedConfig,\n): boolean {\n // Built-in prefixes\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return true;\n }\n // Container query shorthand\n if (key.startsWith('@(')) return true;\n // Config aliases\n return config.states.includes(key);\n}\n\n/**\n * Checks if a CSS selector string is basically valid.\n */\nexport function isValidSelector(selector: string): string | null {\n if (selector.length === 0) return 'Selector cannot be empty';\n\n // Check balanced brackets\n let depth = 0;\n for (const char of selector) {\n if (char === '(' || char === '[') depth++;\n if (char === ')' || char === ']') depth--;\n if (depth < 0) return 'Unbalanced brackets in selector';\n }\n if (depth !== 0) return 'Unbalanced brackets in selector';\n\n return null;\n}\n\n/**\n * Finds a property by key name in an object expression.\n */\nexport function findProperty(\n obj: TSESTree.ObjectExpression,\n name: string,\n): TSESTree.Property | undefined {\n for (const prop of obj.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const keyName = getKeyName(prop.key);\n if (keyName === name) return prop;\n }\n }\n return undefined;\n}\n\n/**\n * Collects local predefined state alias names from a styles ObjectExpression.\n * Local states are top-level `@name` keys with string literal values,\n * mirroring the runtime's `extractLocalPredefinedStates()`.\n */\nexport function collectLocalStateAliases(\n node: TSESTree.ObjectExpression,\n): string[] {\n const aliases: string[] = [];\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!/^@[A-Za-z][A-Za-z0-9-]*$/.test(key)) continue;\n\n // Skip built-in prefixes — they are not local state definitions\n if (BUILT_IN_STATE_PREFIXES.has(key)) continue;\n\n // Must have a string value to be a state definition\n if (getStringValue(prop.value) !== null) {\n aliases.push(key);\n }\n }\n\n return aliases;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { Scope } from '@typescript-eslint/utils/ts-eslint';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n BUILT_IN_STATE_PREFIXES,\n} from './constants.js';\nimport type { ResolvedConfig } from './types.js';\n\n/** Strip TypeScript wrappers that do not change an expression's runtime value. */\nexport function unwrapExpression(node: TSESTree.Node): TSESTree.Node {\n let current = node;\n\n while (\n current.type === 'TSAsExpression' ||\n current.type === 'TSSatisfiesExpression' ||\n current.type === 'TSNonNullExpression' ||\n current.type === 'TSTypeAssertion' ||\n current.type === 'TSInstantiationExpression'\n ) {\n current = current.expression;\n }\n\n return current;\n}\n\n/** A whole-string pattern in which `*` matches any run of characters. */\n/**\n * A variable's one value definition. TypeScript declares a same-named type or\n * interface (`const Box = tasty(…); type Box = …`) on the same variable, so\n * type definitions are set aside; null for a redeclared or type-only name.\n */\nexport function valueDefinition(\n variable: Scope.Variable,\n): Scope.Definition | null {\n const defs = variable.defs.filter((def) => def.type !== 'Type');\n return defs.length === 1 ? defs[0] : null;\n}\n\nexport function wildcardPattern(pattern: string): RegExp {\n const escaped = pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, '.*')}$`);\n}\n\n/**\n * Gets the string value of a property key node.\n */\nexport function getKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n if (key.type === 'Literal' && typeof key.value === 'number')\n return String(key.value);\n return null;\n}\n\n/**\n * Gets the string value of a node if it is a string literal.\n */\nexport function getStringValue(node: TSESTree.Node): string | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return node.value;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return node.quasis[0].value.cooked ?? null;\n }\n return null;\n}\n\n/**\n * Checks if a value node is a static literal.\n */\nexport function isStaticValue(node: TSESTree.Node): boolean {\n node = unwrapExpression(node);\n if (node.type === 'Literal') return true;\n if (\n node.type === 'UnaryExpression' &&\n node.operator === '-' &&\n node.argument.type === 'Literal'\n ) {\n return true;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return true;\n }\n if (node.type === 'ArrayExpression') {\n return node.elements.every((el) => el !== null && isStaticValue(el));\n }\n if (node.type === 'ObjectExpression') {\n return node.properties.every(\n (prop) =>\n prop.type === 'Property' && !prop.computed && isStaticValue(prop.value),\n );\n }\n return false;\n}\n\n/**\n * Validates color token syntax.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateColorTokenSyntax(token: string): string | null {\n // Strip leading # or ##\n let name = token;\n if (name.startsWith('##')) {\n name = name.slice(2);\n } else if (name.startsWith('#')) {\n name = name.slice(1);\n } else {\n return 'Color token must start with #';\n }\n\n if (name.length === 0) return 'Empty color token name';\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) return 'Empty color token name before opacity';\n\n if (opacitySuffix.startsWith('$')) {\n // Dynamic opacity from CSS custom property — always valid\n return null;\n }\n\n if (opacitySuffix.length === 0) return 'Trailing dot with no opacity value';\n\n // Tasty reads the digits as a decimal fraction (`.5` and `.50` are 50%,\n // `.150` is 15%), so any digit string is in range.\n if (!/^\\d+$/.test(opacitySuffix)) {\n return /^-\\d+$/.test(opacitySuffix)\n ? 'Opacity cannot be negative'\n : `Invalid opacity value '${opacitySuffix}'`;\n }\n }\n\n return null;\n}\n\n/**\n * Checks if a string looks like a raw hex color (not a token).\n * Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).\n */\nexport function isRawHexColor(value: string): boolean {\n if (!value.startsWith('#')) return false;\n const hex = value.slice(1).split('.')[0];\n if (![3, 4, 6, 8].includes(hex.length)) return false;\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\n/**\n * Extracts custom unit from a value token like \"2x\", \"1.5r\", \"3cols\".\n * Returns the unit name, or null if not a custom-unit value.\n */\nexport function extractCustomUnit(token: string): string | null {\n const match = token.match(/^-?[\\d.]+([a-zA-Z]+)$/);\n if (!match) return null;\n return match[1];\n}\n\n/**\n * Checks if a unit is valid (built-in, CSS, or in config).\n */\nexport function isValidUnit(unit: string, config: ResolvedConfig): boolean {\n if (config.units === false) return true;\n if (BUILT_IN_UNITS.has(unit)) return true;\n if (CSS_UNITS.has(unit)) return true;\n if (Array.isArray(config.units) && config.units.includes(unit)) return true;\n return false;\n}\n\n/**\n * Checks if a state alias key (starting with @) is known.\n */\nexport function isKnownStateAlias(\n key: string,\n config: ResolvedConfig,\n): boolean {\n // Built-in prefixes\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return true;\n }\n // Container query shorthand\n if (key.startsWith('@(')) return true;\n // Config aliases\n return config.states.includes(key);\n}\n\n/**\n * Checks if a CSS selector string is basically valid.\n */\nexport function isValidSelector(selector: string): string | null {\n if (selector.length === 0) return 'Selector cannot be empty';\n\n // Check balanced brackets\n let depth = 0;\n for (const char of selector) {\n if (char === '(' || char === '[') depth++;\n if (char === ')' || char === ']') depth--;\n if (depth < 0) return 'Unbalanced brackets in selector';\n }\n if (depth !== 0) return 'Unbalanced brackets in selector';\n\n return null;\n}\n\n/**\n * Finds a property by key name in an object expression.\n */\nexport function findProperty(\n obj: TSESTree.ObjectExpression,\n name: string,\n): TSESTree.Property | undefined {\n for (const prop of obj.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const keyName = getKeyName(prop.key);\n if (keyName === name) return prop;\n }\n }\n return undefined;\n}\n\n/**\n * Collects local predefined state alias names from a styles ObjectExpression.\n * Local states are top-level `@name` keys with string literal values,\n * mirroring the runtime's `extractLocalPredefinedStates()`.\n */\nexport function collectLocalStateAliases(\n node: TSESTree.ObjectExpression,\n): string[] {\n const aliases: string[] = [];\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!/^@[A-Za-z][A-Za-z0-9-]*$/.test(key)) continue;\n\n // Skip built-in prefixes — they are not local state definitions\n if (BUILT_IN_STATE_PREFIXES.has(key)) continue;\n\n // Must have a string value to be a state definition\n if (getStringValue(prop.value) !== null) {\n aliases.push(key);\n }\n }\n\n return aliases;\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.12.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@types/node": "^22.0.0",
|
|
57
57
|
"@typescript-eslint/parser": "^8.56.1",
|
|
58
58
|
"@typescript-eslint/rule-tester": "^8.56.1",
|
|
59
|
+
"@webref/css": "^8.7.8",
|
|
59
60
|
"eslint": "^10.0.0",
|
|
60
61
|
"eslint-config-prettier": "^10.1.8",
|
|
61
62
|
"known-css-properties": "^0.37.0",
|