@tenphi/eslint-plugin-tasty 1.5.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +160 -3
  2. package/dist/configs.js +3 -1
  3. package/dist/configs.js.map +1 -1
  4. package/dist/constants.js +1 -1
  5. package/dist/context.js +51 -28
  6. package/dist/context.js.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/rules/consistent-token-usage.js +88 -87
  11. package/dist/rules/consistent-token-usage.js.map +1 -1
  12. package/dist/rules/no-classname-prop.js +27 -0
  13. package/dist/rules/no-classname-prop.js.map +1 -0
  14. package/dist/rules/no-important.js +23 -28
  15. package/dist/rules/no-important.js.map +1 -1
  16. package/dist/rules/no-raw-color-values.js +40 -59
  17. package/dist/rules/no-raw-color-values.js.map +1 -1
  18. package/dist/rules/no-raw-motion-duration.js +6 -24
  19. package/dist/rules/no-raw-motion-duration.js.map +1 -1
  20. package/dist/rules/no-raw-transition-duration.js +7 -49
  21. package/dist/rules/no-raw-transition-duration.js.map +1 -1
  22. package/dist/rules/no-runtime-styles-mutation.js +2 -2
  23. package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
  24. package/dist/rules/no-state-in-selector.js +1 -1
  25. package/dist/rules/no-state-in-selector.js.map +1 -1
  26. package/dist/rules/no-style-prop.js +27 -0
  27. package/dist/rules/no-style-prop.js.map +1 -0
  28. package/dist/rules/no-style-spread.js +2 -2
  29. package/dist/rules/no-style-spread.js.map +1 -1
  30. package/dist/rules/no-styles-prop.js +7 -5
  31. package/dist/rules/no-styles-prop.js.map +1 -1
  32. package/dist/rules/prefer-auto-calc.js +5 -27
  33. package/dist/rules/prefer-auto-calc.js.map +1 -1
  34. package/dist/rules/prefer-custom-property-syntax.js +12 -31
  35. package/dist/rules/prefer-custom-property-syntax.js.map +1 -1
  36. package/dist/rules/prefer-shorthand-property.js +2 -2
  37. package/dist/rules/prefer-shorthand-property.js.map +1 -1
  38. package/dist/rules/tasty-component-props.js +33 -0
  39. package/dist/rules/tasty-component-props.js.map +1 -0
  40. package/dist/rules/valid-boolean-property.js +6 -26
  41. package/dist/rules/valid-boolean-property.js.map +1 -1
  42. package/dist/rules/valid-color-token.js +12 -29
  43. package/dist/rules/valid-color-token.js.map +1 -1
  44. package/dist/rules/valid-custom-property.js +12 -28
  45. package/dist/rules/valid-custom-property.js.map +1 -1
  46. package/dist/rules/valid-custom-unit.js +10 -27
  47. package/dist/rules/valid-custom-unit.js.map +1 -1
  48. package/dist/rules/valid-directional-modifier.js +5 -27
  49. package/dist/rules/valid-directional-modifier.js.map +1 -1
  50. package/dist/rules/valid-preset.js +5 -29
  51. package/dist/rules/valid-preset.js.map +1 -1
  52. package/dist/rules/valid-radius-shape.js +5 -25
  53. package/dist/rules/valid-radius-shape.js.map +1 -1
  54. package/dist/rules/valid-recipe.js +5 -20
  55. package/dist/rules/valid-recipe.js.map +1 -1
  56. package/dist/rules/valid-transition.js +5 -25
  57. package/dist/rules/valid-transition.js.map +1 -1
  58. package/dist/rules/valid-value.js +5 -31
  59. package/dist/rules/valid-value.js.map +1 -1
  60. package/dist/style-values.js +79 -0
  61. package/dist/style-values.js.map +1 -0
  62. package/dist/type-aware-jsx.d.ts +9 -0
  63. package/dist/type-aware-jsx.js +275 -0
  64. package/dist/type-aware-jsx.js.map +1 -0
  65. package/dist/utils.js +1 -10
  66. package/dist/utils.js.map +1 -1
  67. package/dist/value-words.js +76 -0
  68. package/dist/value-words.js.map +1 -0
  69. package/package.json +6 -2
@@ -0,0 +1,275 @@
1
+ import { createRequire } from "node:module";
2
+ import { statSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ //#region src/type-aware-jsx.ts
6
+ const require = createRequire(import.meta.url);
7
+ let typescript;
8
+ const projects = /* @__PURE__ */ new Map();
9
+ const classifiers = /* @__PURE__ */ new WeakMap();
10
+ function getTypeScript() {
11
+ if (!typescript) try {
12
+ typescript = require("typescript");
13
+ } catch {
14
+ throw new Error("tasty: typeAwareJSX requires TypeScript. Install typescript as a dev dependency.");
15
+ }
16
+ return typescript;
17
+ }
18
+ function version(filename) {
19
+ try {
20
+ const stat = statSync(filename);
21
+ return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;
22
+ } catch {
23
+ return "missing";
24
+ }
25
+ }
26
+ function changed(versions) {
27
+ for (const [filename, previous] of versions) if (version(filename) !== previous) return true;
28
+ return false;
29
+ }
30
+ function readConfig(T, configPath, configVersions) {
31
+ const config = T.getParsedCommandLineOfConfigFile(configPath, {}, {
32
+ ...T.sys,
33
+ readFile(filename) {
34
+ configVersions.set(filename, version(filename));
35
+ return T.sys.readFile(filename);
36
+ },
37
+ onUnRecoverableConfigFileDiagnostic(diagnostic) {
38
+ throw new Error(`tasty: ${T.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`);
39
+ }
40
+ });
41
+ if (!config || config.errors.length) throw new Error(`tasty: invalid typeAwareJSX project ${configPath}: ${config?.errors.map((d) => T.flattenDiagnosticMessageText(d.messageText, "\n")).join("\n")}`);
42
+ return config;
43
+ }
44
+ function readProject(T, configPath) {
45
+ const configVersions = /* @__PURE__ */ new Map();
46
+ return {
47
+ config: readConfig(T, configPath, configVersions),
48
+ configVersions,
49
+ versions: /* @__PURE__ */ new Map(),
50
+ sourceFiles: /* @__PURE__ */ new Map(),
51
+ excludedFiles: /* @__PURE__ */ new Map()
52
+ };
53
+ }
54
+ function getProgram(T, configPath, filename, text) {
55
+ let project = projects.get(configPath);
56
+ let rootsChanged = false;
57
+ if (!project || changed(project.configVersions)) project = readProject(T, configPath);
58
+ else if (project.program && !project.program.getSourceFile(filename) && project.excludedFiles.get(filename) !== version(filename)) {
59
+ const refreshed = readProject(T, configPath);
60
+ const previousFiles = project.config.fileNames;
61
+ rootsChanged = refreshed.config.fileNames.length !== previousFiles.length || refreshed.config.fileNames.some((file, index) => file !== previousFiles[index]);
62
+ if (rootsChanged) project.config = refreshed.config;
63
+ if (!project.config.fileNames.includes(filename)) project.excludedFiles.set(filename, version(filename));
64
+ else project.excludedFiles.delete(filename);
65
+ }
66
+ projects.delete(configPath);
67
+ projects.set(configPath, project);
68
+ if (projects.size > 3) projects.delete(projects.keys().next().value);
69
+ const sourcesChanged = changed(project.versions);
70
+ const overlay = project.excludedFiles.has(filename) && !project.program?.getSourceFile(filename) && !rootsChanged && !sourcesChanged ? void 0 : T.sys.readFile(filename) === text ? void 0 : {
71
+ filename,
72
+ text
73
+ };
74
+ const overlayChanged = project.overlay?.filename !== overlay?.filename || project.overlay?.text !== overlay?.text;
75
+ if (!project.program || rootsChanged || overlayChanged || sourcesChanged) {
76
+ const host = T.createCompilerHost(project.config.options, true);
77
+ host.getParsedCommandLine = (path) => readConfig(T, path, project.configVersions);
78
+ const readFile = host.readFile;
79
+ host.readFile = (path) => resolve(path) === overlay?.filename ? overlay.text : readFile(path);
80
+ const getSourceFile = host.getSourceFile;
81
+ host.getSourceFile = (path, options, onError, force) => {
82
+ const key = resolve(path);
83
+ const stamp = version(key);
84
+ const overlayText = key === overlay?.filename ? overlay.text : void 0;
85
+ const parseOptions = typeof options === "number" ? { languageVersion: options } : options;
86
+ const parseKey = [
87
+ parseOptions.languageVersion,
88
+ parseOptions.impliedNodeFormat,
89
+ parseOptions.jsDocParsingMode
90
+ ].join(":");
91
+ const cached = project.sourceFiles.get(key);
92
+ if (!force && cached?.version === stamp && cached.overlayText === overlayText && cached.parseKey === parseKey) return cached.source;
93
+ const source = getSourceFile(path, options, onError, force);
94
+ if (source) project.sourceFiles.set(key, {
95
+ version: stamp,
96
+ overlayText,
97
+ parseKey,
98
+ source
99
+ });
100
+ else project.sourceFiles.delete(key);
101
+ return source;
102
+ };
103
+ project.program = T.createProgram({
104
+ rootNames: project.config.fileNames,
105
+ options: project.config.options,
106
+ projectReferences: project.config.projectReferences,
107
+ host,
108
+ oldProgram: project.program
109
+ });
110
+ project.overlay = project.program.getSourceFile(filename) ? overlay : void 0;
111
+ if (!project.program.getSourceFile(filename)) project.excludedFiles.set(filename, version(filename));
112
+ project.versions = new Map(project.program.getSourceFiles().map((file) => [file.fileName, version(file.fileName)]));
113
+ const retained = new Set(project.program.getSourceFiles().map((file) => resolve(file.fileName)));
114
+ for (const path of project.sourceFiles.keys()) if (!retained.has(path)) project.sourceFiles.delete(path);
115
+ }
116
+ return project.program;
117
+ }
118
+ function makeClassifier(T, program, filename, text) {
119
+ const source = program.getSourceFile(filename);
120
+ if (!source || source.text !== text) return () => "unknown";
121
+ const checker = program.getTypeChecker();
122
+ const modulePath = T.resolveModuleName("@tenphi/tasty", filename, program.getCompilerOptions(), T.sys).resolvedModule?.resolvedFileName;
123
+ const moduleFile = modulePath && program.getSourceFile(modulePath);
124
+ const module = moduleFile && checker.getSymbolAtLocation(moduleFile);
125
+ const exports = module ? checker.getExportsOfModule(module) : [];
126
+ const styleTypeSymbols = new Set(exports?.filter((s) => [
127
+ "Styles",
128
+ "StyleValue",
129
+ "StyleValueStateMap"
130
+ ].includes(s.name)).map((s) => s.flags & T.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s));
131
+ let symbol = exports?.find((s) => s.name === "Styles");
132
+ if (symbol && symbol.flags & T.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
133
+ const styles = symbol && checker.getDeclaredTypeOfSymbol(symbol);
134
+ if (!styles || styles.flags & (T.TypeFlags.Any | T.TypeFlags.Unknown)) return () => "unknown";
135
+ const styleDeclarations = new Set(styles.getProperties().flatMap((property) => property.declarations ?? []));
136
+ function isStyleDeclaration(declaration) {
137
+ return styleDeclarations.has(declaration) || /[/\\]node_modules[/\\](?:@tenphi[/\\]tasty|csstype)[/\\]/.test(declaration.getSourceFile().fileName);
138
+ }
139
+ const attributes = /* @__PURE__ */ new Map();
140
+ function collect(node) {
141
+ if (T.isJsxAttribute(node)) attributes.set(node.getStart(source), node);
142
+ T.forEachChild(node, collect);
143
+ }
144
+ collect(source);
145
+ function referencesStyles(node, visited = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Map()) {
146
+ if (visited.has(node)) return false;
147
+ visited.add(node);
148
+ try {
149
+ if (T.isPropertySignature(node) || T.isPropertyDeclaration(node) || T.isParameter(node) || T.isTypeAliasDeclaration(node) || T.isMappedTypeNode(node) || T.isParenthesizedTypeNode(node)) return node.type ? referencesStyles(node.type, visited, bindings) : false;
150
+ if (T.isIndexedAccessTypeNode(node)) {
151
+ const object = checker.getTypeFromTypeNode(node.objectType);
152
+ const index = checker.getTypeFromTypeNode(node.indexType);
153
+ const keys = index.isUnion() ? index.types : [index];
154
+ return object === styles || keys.some((key) => {
155
+ if (!key.isStringLiteral() && !key.isNumberLiteral()) return false;
156
+ return checker.getPropertyOfType(object, String(key.value))?.declarations?.some((declaration) => isStyleDeclaration(declaration) || referencesStyles(declaration, visited, bindings));
157
+ });
158
+ }
159
+ if (T.isTypeReferenceNode(node) || T.isImportTypeNode(node)) {
160
+ const name = T.isTypeReferenceNode(node) ? node.typeName : node.qualifier;
161
+ let reference = name && checker.getSymbolAtLocation(name);
162
+ if (reference && reference.flags & T.SymbolFlags.Alias) reference = checker.getAliasedSymbol(reference);
163
+ if (reference && styleTypeSymbols.has(reference)) return true;
164
+ if (reference?.declarations?.some(isStyleDeclaration)) return true;
165
+ const bound = reference && bindings.get(reference);
166
+ if (bound) return referencesStyles(bound, visited, bindings);
167
+ if (reference?.declarations?.some((declaration) => {
168
+ if (!T.isTypeAliasDeclaration(declaration)) return false;
169
+ const local = new Map(bindings);
170
+ declaration.typeParameters?.forEach((parameter, index) => {
171
+ const symbol = checker.getSymbolAtLocation(parameter.name);
172
+ const argument = node.typeArguments?.[index] ?? parameter.default;
173
+ if (symbol && argument) local.set(symbol, argument);
174
+ });
175
+ return referencesStyles(declaration.type, visited, local);
176
+ })) return true;
177
+ }
178
+ if (T.isConditionalTypeNode(node)) {
179
+ const parameter = (branch) => {
180
+ if (!T.isTypeReferenceNode(branch)) return void 0;
181
+ const symbol = checker.getSymbolAtLocation(branch.typeName);
182
+ return symbol && symbol.flags & T.SymbolFlags.TypeParameter ? symbol : void 0;
183
+ };
184
+ const trueParameter = parameter(node.trueType);
185
+ const falseParameter = parameter(node.falseType);
186
+ const preservesValue = trueParameter && node.falseType.kind === T.SyntaxKind.NeverKeyword || falseParameter && node.trueType.kind === T.SyntaxKind.NeverKeyword || trueParameter && trueParameter === falseParameter;
187
+ return Boolean(preservesValue) && (referencesStyles(node.trueType, visited, bindings) || referencesStyles(node.falseType, visited, bindings));
188
+ }
189
+ return (T.isUnionTypeNode(node) || T.isIntersectionTypeNode(node)) && node.types.some((part) => referencesStyles(part, visited, bindings));
190
+ } finally {
191
+ visited.delete(node);
192
+ }
193
+ }
194
+ function unresolved(type) {
195
+ if (type.flags & (T.TypeFlags.Any | T.TypeFlags.Unknown | T.TypeFlags.TypeParameter | T.TypeFlags.Conditional | T.TypeFlags.IndexedAccess | T.TypeFlags.Never)) return true;
196
+ return type.isUnionOrIntersection() && type.types.some(unresolved);
197
+ }
198
+ function hasPrimitiveFlag(type, flags) {
199
+ return Boolean(type.flags & flags) || type.isIntersection() && type.types.some((part) => hasPrimitiveFlag(part, flags));
200
+ }
201
+ function unresolvedComponentProp(node, name) {
202
+ const tag = node.parent.parent.tagName;
203
+ const component = checker.getTypeAtLocation(tag);
204
+ return [...component.getCallSignatures(), ...component.getConstructSignatures()].some((signature) => {
205
+ const parameter = signature.parameters[0];
206
+ if (!parameter) return false;
207
+ const props = checker.getTypeOfSymbolAtLocation(parameter, tag);
208
+ const property = checker.getPropertyOfType(props, name);
209
+ return property ? unresolved(checker.getTypeOfSymbolAtLocation(property, tag)) : false;
210
+ });
211
+ }
212
+ const results = /* @__PURE__ */ new Map();
213
+ return (attribute) => {
214
+ const start = attribute.range[0];
215
+ const cached = results.get(start);
216
+ if (cached) return cached;
217
+ const node = attributes.get(start);
218
+ let kind = "unknown";
219
+ if (node && node.end === attribute.range[1] && T.isIdentifier(node.name)) {
220
+ const props = checker.getContextualType(node.parent);
221
+ const property = props && checker.getPropertyOfType(props, node.name.text);
222
+ const declarations = property?.declarations;
223
+ if (property && declarations?.length) {
224
+ const type = checker.getTypeOfSymbolAtLocation(property, node);
225
+ if (declarations.some((d) => isStyleDeclaration(d) || referencesStyles(d))) kind = "style";
226
+ else if (!unresolved(type) && !(declarations.some((declaration) => unresolved(checker.getTypeAtLocation(declaration))) && unresolvedComponentProp(node, node.name.text))) {
227
+ const nonNullable = checker.getNonNullableType(type);
228
+ const parts = nonNullable.isUnion() ? nonNullable.types : [nonNullable];
229
+ const primitive = T.TypeFlags.StringLike | T.TypeFlags.NumberLike | T.TypeFlags.BooleanLike | T.TypeFlags.BigIntLike;
230
+ const broad = T.TypeFlags.String | T.TypeFlags.Number | T.TypeFlags.Boolean | T.TypeFlags.BigInt;
231
+ if (!(parts.every((part) => hasPrimitiveFlag(part, primitive)) && (Boolean(nonNullable.flags & broad) || parts.some((part) => hasPrimitiveFlag(part, broad)) || parts.every((part) => hasPrimitiveFlag(part, T.TypeFlags.BooleanLike))))) kind = "component";
232
+ }
233
+ }
234
+ }
235
+ results.set(start, kind);
236
+ return kind;
237
+ };
238
+ }
239
+ /** No compiler is loaded, or program created, unless the setting is enabled. */
240
+ function jsxPropKind(context, attribute) {
241
+ const option = context.settings.tasty?.typeAwareJSX;
242
+ if (option === void 0 || option === false) return "unknown";
243
+ 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 }.");
244
+ const filename = resolve(context.cwd, context.filename);
245
+ const explicit = option === true ? void 0 : option.project;
246
+ const sourceCode = context.sourceCode;
247
+ const key = explicit ? resolve(context.cwd, explicit) : "nearest";
248
+ let byProject = classifiers.get(sourceCode);
249
+ if (!byProject) {
250
+ byProject = /* @__PURE__ */ new Map();
251
+ classifiers.set(sourceCode, byProject);
252
+ }
253
+ let cached = byProject.get(key);
254
+ if (!cached || cached.filename !== filename || cached.text !== sourceCode.text) {
255
+ const T = getTypeScript();
256
+ const configPath = explicit ? key : T.findConfigFile(dirname(filename), T.sys.fileExists);
257
+ if (!configPath) throw new Error(`tasty: typeAwareJSX could not find a tsconfig for ${filename}.`);
258
+ const program = getProgram(T, configPath, filename, sourceCode.text);
259
+ cached = {
260
+ filename,
261
+ text: sourceCode.text,
262
+ classify: makeClassifier(T, program, filename, sourceCode.text)
263
+ };
264
+ byProject.set(key, cached);
265
+ }
266
+ return cached.classify(attribute);
267
+ }
268
+ /** A SourceCode object may be reused by ESLint after dependencies change. */
269
+ function finishJSXAnalysis(context) {
270
+ classifiers.delete(context.sourceCode);
271
+ }
272
+
273
+ //#endregion
274
+ export { finishJSXAnalysis, jsxPropKind };
275
+ //# sourceMappingURL=type-aware-jsx.js.map
@@ -0,0 +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 try {\n typescript = require('typescript') as TypeScript;\n } catch {\n throw new Error(\n 'tasty: typeAwareJSX requires TypeScript. Install typescript as a dev dependency.',\n );\n }\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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"}
package/dist/utils.js CHANGED
@@ -76,15 +76,6 @@ function isRawHexColor(value) {
76
76
  return /^[0-9a-fA-F]+$/.test(hex);
77
77
  }
78
78
  /**
79
- * Extracts custom unit from a value token like "2x", "1.5r", "3cols".
80
- * Returns the unit name, or null if not a custom-unit value.
81
- */
82
- function extractCustomUnit(token) {
83
- const match = token.match(/^-?[\d.]+([a-zA-Z]+)$/);
84
- if (!match) return null;
85
- return match[1];
86
- }
87
- /**
88
79
  * Checks if a unit is valid (built-in, CSS, or in config).
89
80
  */
90
81
  function isValidUnit(unit, config) {
@@ -135,5 +126,5 @@ function collectLocalStateAliases(node) {
135
126
  }
136
127
 
137
128
  //#endregion
138
- export { collectLocalStateAliases, extractCustomUnit, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, unwrapExpression, validateColorTokenSyntax };
129
+ export { collectLocalStateAliases, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, unwrapExpression, validateColorTokenSyntax };
139
130
  //# 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/**\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 const opacity = Number(opacitySuffix);\n if (isNaN(opacity)) return `Invalid opacity value '${opacitySuffix}'`;\n if (opacity < 0) return 'Opacity cannot be negative';\n if (opacity > 100) return `Opacity '${opacitySuffix}' exceeds 100`;\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 {\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/**\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 const opacity = Number(opacitySuffix);\n if (isNaN(opacity)) return `Invalid opacity value '${opacitySuffix}'`;\n if (opacity < 0) return 'Opacity cannot be negative';\n if (opacity > 100) return `Opacity '${opacitySuffix}' exceeds 100`;\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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"}
@@ -0,0 +1,76 @@
1
+ //#region src/value-words.ts
2
+ function scan(value) {
3
+ const words = [];
4
+ const opaque = [];
5
+ const pattern = /(?:[#$]{1,2}[\w-]*(?:\.[\w$.-]*)?|[+-]?(?:\d*\.\d+|\d+\.?\d*)(?:e[+-]?\d+)?(?:[a-z][a-z0-9]*|%)?|[a-z_][\w-]*|--[\w-]+)/iy;
6
+ let i = 0;
7
+ const skipQuote = () => {
8
+ const quote = value[i++];
9
+ while (i < value.length) {
10
+ const ch = value[i++];
11
+ if (ch === "\\") i++;
12
+ else if (ch === quote) break;
13
+ }
14
+ i = Math.min(i, value.length);
15
+ };
16
+ while (i < value.length) {
17
+ if (value[i] === "\"" || value[i] === "'") {
18
+ const start = i;
19
+ skipQuote();
20
+ opaque.push({
21
+ start,
22
+ end: i
23
+ });
24
+ continue;
25
+ }
26
+ pattern.lastIndex = i;
27
+ const match = pattern.exec(value);
28
+ if (!match) {
29
+ i++;
30
+ continue;
31
+ }
32
+ const word = match[0];
33
+ const offset = i;
34
+ i += word.length;
35
+ const paren = /^\s*\(/.exec(value.slice(i));
36
+ if (word.toLowerCase() === "url" && paren) {
37
+ i += paren[0].length;
38
+ while (i < value.length) if (value[i] === "\"" || value[i] === "'") skipQuote();
39
+ else if (value[i] === "\\") i += 2;
40
+ else if (value[i++] === ")") break;
41
+ i = Math.min(i, value.length);
42
+ opaque.push({
43
+ start: offset,
44
+ end: i
45
+ });
46
+ continue;
47
+ }
48
+ words.push({
49
+ value: word,
50
+ offset,
51
+ isFunction: paren !== null
52
+ });
53
+ }
54
+ return {
55
+ words,
56
+ opaque
57
+ };
58
+ }
59
+ /** Scan inside expressions while keeping strings, URLs, and identifiers opaque. */
60
+ function scanValueWords(value) {
61
+ return scan(value).words;
62
+ }
63
+ /** Hide text that regex-based checks must not rewrite, preserving source offsets. */
64
+ function maskValueLiterals(value) {
65
+ let result = "";
66
+ let start = 0;
67
+ for (const span of scan(value).opaque) {
68
+ result += value.slice(start, span.start) + " ".repeat(span.end - span.start);
69
+ start = span.end;
70
+ }
71
+ return result + value.slice(start);
72
+ }
73
+
74
+ //#endregion
75
+ export { maskValueLiterals, scanValueWords };
76
+ //# sourceMappingURL=value-words.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-words.js","names":[],"sources":["../src/value-words.ts"],"sourcesContent":["export interface ValueWord {\n value: string;\n offset: number;\n isFunction: boolean;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\nfunction scan(value: string): { words: ValueWord[]; opaque: Span[] } {\n const words: ValueWord[] = [];\n const opaque: Span[] = [];\n // A whole identifier must win over a color name or number inside it.\n const pattern =\n /(?:[#$]{1,2}[\\w-]*(?:\\.[\\w$.-]*)?|[+-]?(?:\\d*\\.\\d+|\\d+\\.?\\d*)(?:e[+-]?\\d+)?(?:[a-z][a-z0-9]*|%)?|[a-z_][\\w-]*|--[\\w-]+)/iy;\n let i = 0;\n const skipQuote = () => {\n const quote = value[i++];\n while (i < value.length) {\n const ch = value[i++];\n if (ch === '\\\\') i++;\n else if (ch === quote) break;\n }\n i = Math.min(i, value.length);\n };\n while (i < value.length) {\n if (value[i] === '\"' || value[i] === \"'\") {\n const start = i;\n skipQuote();\n opaque.push({ start, end: i });\n continue;\n }\n pattern.lastIndex = i;\n const match = pattern.exec(value);\n if (!match) {\n i++;\n continue;\n }\n const word = match[0];\n const offset = i;\n i += word.length;\n const paren = /^\\s*\\(/.exec(value.slice(i));\n if (word.toLowerCase() === 'url' && paren) {\n i += paren[0].length;\n while (i < value.length) {\n if (value[i] === '\"' || value[i] === \"'\") skipQuote();\n else if (value[i] === '\\\\') i += 2;\n else if (value[i++] === ')') break;\n }\n i = Math.min(i, value.length);\n opaque.push({ start: offset, end: i });\n continue;\n }\n words.push({ value: word, offset, isFunction: paren !== null });\n }\n return { words, opaque };\n}\n\n/** Scan inside expressions while keeping strings, URLs, and identifiers opaque. */\nexport function scanValueWords(value: string): ValueWord[] {\n return scan(value).words;\n}\n\n/** Hide text that regex-based checks must not rewrite, preserving source offsets. */\nexport function maskValueLiterals(value: string): string {\n let result = '';\n let start = 0;\n for (const span of scan(value).opaque) {\n result +=\n value.slice(start, span.start) + ' '.repeat(span.end - span.start);\n start = span.end;\n }\n return result + value.slice(start);\n}\n"],"mappings":";AAWA,SAAS,KAAK,OAAuD;CACnE,MAAM,QAAqB,EAAE;CAC7B,MAAM,SAAiB,EAAE;CAEzB,MAAM,UACJ;CACF,IAAI,IAAI;CACR,MAAM,kBAAkB;EACtB,MAAM,QAAQ,MAAM;AACpB,SAAO,IAAI,MAAM,QAAQ;GACvB,MAAM,KAAK,MAAM;AACjB,OAAI,OAAO,KAAM;YACR,OAAO,MAAO;;AAEzB,MAAI,KAAK,IAAI,GAAG,MAAM,OAAO;;AAE/B,QAAO,IAAI,MAAM,QAAQ;AACvB,MAAI,MAAM,OAAO,QAAO,MAAM,OAAO,KAAK;GACxC,MAAM,QAAQ;AACd,cAAW;AACX,UAAO,KAAK;IAAE;IAAO,KAAK;IAAG,CAAC;AAC9B;;AAEF,UAAQ,YAAY;EACpB,MAAM,QAAQ,QAAQ,KAAK,MAAM;AACjC,MAAI,CAAC,OAAO;AACV;AACA;;EAEF,MAAM,OAAO,MAAM;EACnB,MAAM,SAAS;AACf,OAAK,KAAK;EACV,MAAM,QAAQ,SAAS,KAAK,MAAM,MAAM,EAAE,CAAC;AAC3C,MAAI,KAAK,aAAa,KAAK,SAAS,OAAO;AACzC,QAAK,MAAM,GAAG;AACd,UAAO,IAAI,MAAM,OACf,KAAI,MAAM,OAAO,QAAO,MAAM,OAAO,IAAK,YAAW;YAC5C,MAAM,OAAO,KAAM,MAAK;YACxB,MAAM,SAAS,IAAK;AAE/B,OAAI,KAAK,IAAI,GAAG,MAAM,OAAO;AAC7B,UAAO,KAAK;IAAE,OAAO;IAAQ,KAAK;IAAG,CAAC;AACtC;;AAEF,QAAM,KAAK;GAAE,OAAO;GAAM;GAAQ,YAAY,UAAU;GAAM,CAAC;;AAEjE,QAAO;EAAE;EAAO;EAAQ;;;AAI1B,SAAgB,eAAe,OAA4B;AACzD,QAAO,KAAK,MAAM,CAAC;;;AAIrB,SAAgB,kBAAkB,OAAuB;CACvD,IAAI,SAAS;CACb,IAAI,QAAQ;AACZ,MAAK,MAAM,QAAQ,KAAK,MAAM,CAAC,QAAQ;AACrC,YACE,MAAM,MAAM,OAAO,KAAK,MAAM,GAAG,IAAI,OAAO,KAAK,MAAM,KAAK,MAAM;AACpE,UAAQ,KAAK;;AAEf,QAAO,SAAS,MAAM,MAAM,MAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.5.2",
3
+ "version": "1.7.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -40,7 +40,8 @@
40
40
  "homepage": "https://github.com/tenphi/eslint-plugin-tasty#readme",
41
41
  "peerDependencies": {
42
42
  "@tenphi/tasty": ">=3.8.0",
43
- "eslint": ">=8.0.0"
43
+ "eslint": ">=8.0.0",
44
+ "typescript": ">=5.6.3 <6"
44
45
  },
45
46
  "dependencies": {
46
47
  "@typescript-eslint/utils": "^8.56.0",
@@ -67,6 +68,9 @@
67
68
  "vitest": "^4.0.18"
68
69
  },
69
70
  "peerDependenciesMeta": {
71
+ "typescript": {
72
+ "optional": true
73
+ },
70
74
  "@tenphi/tasty": {
71
75
  "optional": true
72
76
  }