@tenphi/eslint-plugin-tasty 1.0.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -4
- package/dist/config.js +8 -2
- package/dist/config.js.map +1 -1
- package/dist/configs.js +1 -0
- package/dist/configs.js.map +1 -1
- package/dist/constants.js +294 -38
- package/dist/constants.js.map +1 -1
- package/dist/context.js +75 -25
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/property-expectations.js +71 -2
- package/dist/property-expectations.js.map +1 -1
- package/dist/rules/consistent-token-usage.js +12 -1
- package/dist/rules/consistent-token-usage.js.map +1 -1
- package/dist/rules/motion-duration-utils.js +30 -0
- package/dist/rules/motion-duration-utils.js.map +1 -0
- package/dist/rules/no-own-at-root.js +1 -9
- package/dist/rules/no-own-at-root.js.map +1 -1
- package/dist/rules/no-raw-motion-duration.js +105 -0
- package/dist/rules/no-raw-motion-duration.js.map +1 -0
- package/dist/rules/no-raw-transition-duration.js +156 -0
- package/dist/rules/no-raw-transition-duration.js.map +1 -0
- package/dist/rules/no-unknown-state-alias.js +2 -2
- package/dist/rules/no-unknown-state-alias.js.map +1 -1
- package/dist/rules/valid-boolean-property.js +19 -8
- package/dist/rules/valid-boolean-property.js.map +1 -1
- package/dist/rules/valid-directional-modifier.js +16 -1
- package/dist/rules/valid-directional-modifier.js.map +1 -1
- package/dist/rules/valid-state-key.js +4 -13
- package/dist/rules/valid-state-key.js.map +1 -1
- package/dist/rules/valid-sub-element.js +5 -4
- package/dist/rules/valid-sub-element.js.map +1 -1
- package/dist/types.d.ts +14 -1
- package/dist/utils.js +8 -21
- package/dist/utils.js.map +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
|
+
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import { scanTokens } from "../parsers/utils.js";
|
|
5
|
+
import { replaceInStringValue } from "../fix-utils.js";
|
|
6
|
+
import { getDurationTokens, hasRawTimeInExpression, isNonzeroTime, isTimeExpression, isTimeLiteral, isZeroTime } from "./motion-duration-utils.js";
|
|
7
|
+
|
|
8
|
+
//#region src/rules/no-raw-motion-duration.ts
|
|
9
|
+
const PROPERTIES = new Set([
|
|
10
|
+
"animation",
|
|
11
|
+
"animationDuration",
|
|
12
|
+
"transitionDuration"
|
|
13
|
+
]);
|
|
14
|
+
var no_raw_motion_duration_default = createRule({
|
|
15
|
+
name: "no-raw-motion-duration",
|
|
16
|
+
meta: {
|
|
17
|
+
type: "suggestion",
|
|
18
|
+
hasSuggestions: true,
|
|
19
|
+
docs: { description: "Suggest motion tokens for animation and explicit CSS duration properties" },
|
|
20
|
+
messages: {
|
|
21
|
+
rawMotionDuration: "{{property}} duration '{{duration}}' is hardcoded. Use a duration token.",
|
|
22
|
+
useDurationToken: "Replace '{{duration}}' with '{{token}}'"
|
|
23
|
+
},
|
|
24
|
+
schema: []
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
const ctx = new TastyContext(context);
|
|
29
|
+
const durationTokens = getDurationTokens(ctx.config.tokens);
|
|
30
|
+
function checkDuration(token, property, node) {
|
|
31
|
+
const value = token.value;
|
|
32
|
+
if (!isNonzeroTime(value) && !hasRawTimeInExpression(value)) return;
|
|
33
|
+
const candidates = property === "transitionDuration" ? durationTokens : durationTokens.filter((name) => /animation|duration/i.test(name));
|
|
34
|
+
const isLiteral = isTimeLiteral(value);
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
messageId: "rawMotionDuration",
|
|
38
|
+
data: {
|
|
39
|
+
property,
|
|
40
|
+
duration: value
|
|
41
|
+
},
|
|
42
|
+
suggest: isLiteral ? candidates.map((candidate) => ({
|
|
43
|
+
messageId: "useDurationToken",
|
|
44
|
+
data: {
|
|
45
|
+
duration: value,
|
|
46
|
+
token: candidate
|
|
47
|
+
},
|
|
48
|
+
fix: (fixer) => replaceInStringValue(fixer, node, [{
|
|
49
|
+
start: token.offset,
|
|
50
|
+
end: token.offset + value.length,
|
|
51
|
+
replacement: candidate
|
|
52
|
+
}], context.sourceCode)
|
|
53
|
+
})) : []
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function checkGroup(group, property, node) {
|
|
57
|
+
for (const token of group) {
|
|
58
|
+
if (property === "animation" && (/^\$[\w-]+$/.test(token.value) || /^var\(/i.test(token.value) || isZeroTime(token.value) || isTimeExpression(token.value) && !hasRawTimeInExpression(token.value))) return;
|
|
59
|
+
if (isTimeLiteral(token.value)) {
|
|
60
|
+
checkDuration(token, property, node);
|
|
61
|
+
if (property === "animation") return;
|
|
62
|
+
} else if (hasRawTimeInExpression(token.value)) {
|
|
63
|
+
checkDuration(token, property, node);
|
|
64
|
+
if (property === "animation") return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function checkValue(value, property, node) {
|
|
69
|
+
let group = [];
|
|
70
|
+
for (const token of scanTokens(value)) {
|
|
71
|
+
if (token.value) group.push(token);
|
|
72
|
+
if (token.isComma) {
|
|
73
|
+
checkGroup(group, property, node);
|
|
74
|
+
group = [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
checkGroup(group, property, node);
|
|
78
|
+
}
|
|
79
|
+
function handleStyleObject(node) {
|
|
80
|
+
if (!ctx.isStyleObject(node)) return;
|
|
81
|
+
for (const prop of node.properties) {
|
|
82
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
83
|
+
const key = getKeyName(prop.key);
|
|
84
|
+
if (!key || !PROPERTIES.has(key)) continue;
|
|
85
|
+
const value = getStringValue(prop.value);
|
|
86
|
+
if (value) checkValue(value, key, prop.value);
|
|
87
|
+
else if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
|
|
88
|
+
if (stateProp.type !== "Property") continue;
|
|
89
|
+
const stateValue = getStringValue(stateProp.value);
|
|
90
|
+
if (stateValue) checkValue(stateValue, key, stateProp.value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ImportDeclaration(node) {
|
|
96
|
+
ctx.trackImport(node);
|
|
97
|
+
},
|
|
98
|
+
...styleObjectListeners(handleStyleObject)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { no_raw_motion_duration_default as default };
|
|
105
|
+
//# sourceMappingURL=no-raw-motion-duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-raw-motion-duration.js","names":[],"sources":["../../src/rules/no-raw-motion-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanTokens, type ScannedToken } from '../parsers/utils.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport {\n getDurationTokens,\n hasRawTimeInExpression,\n isNonzeroTime,\n isTimeExpression,\n isTimeLiteral,\n isZeroTime,\n} from './motion-duration-utils.js';\n\ntype MessageIds = 'rawMotionDuration' | 'useDurationToken';\n\nconst PROPERTIES = new Set([\n 'animation',\n 'animationDuration',\n 'transitionDuration',\n]);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-motion-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest motion tokens for animation and explicit CSS duration properties',\n },\n messages: {\n rawMotionDuration:\n \"{{property}} duration '{{duration}}' is hardcoded. Use a duration token.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const durationTokens = getDurationTokens(ctx.config.tokens);\n\n function checkDuration(\n token: ScannedToken,\n property: string,\n node: TSESTree.Node,\n ): void {\n const value = token.value;\n if (!isNonzeroTime(value) && !hasRawTimeInExpression(value)) return;\n\n // Animation has no implicit timing token. Only names that read as an\n // animation or generic duration are useful suggestions for that property.\n const candidates =\n property === 'transitionDuration'\n ? durationTokens\n : durationTokens.filter((name) => /animation|duration/i.test(name));\n const isLiteral = isTimeLiteral(value);\n context.report({\n node,\n messageId: 'rawMotionDuration',\n data: { property, duration: value },\n suggest: isLiteral\n ? candidates.map((candidate) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: value, token: candidate },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [\n {\n start: token.offset,\n end: token.offset + value.length,\n replacement: candidate,\n },\n ],\n context.sourceCode,\n ),\n }))\n : [],\n });\n }\n\n function checkGroup(\n group: ScannedToken[],\n property: string,\n node: TSESTree.Node,\n ): void {\n for (const token of group) {\n // The first time in an animation shorthand is its duration; a second\n // time is a delay. A variable can also be a duration, so stop there\n // rather than misreporting a later literal delay.\n if (\n property === 'animation' &&\n (/^\\$[\\w-]+$/.test(token.value) ||\n /^var\\(/i.test(token.value) ||\n isZeroTime(token.value) ||\n (isTimeExpression(token.value) &&\n !hasRawTimeInExpression(token.value)))\n ) {\n return;\n }\n if (isTimeLiteral(token.value)) {\n checkDuration(token, property, node);\n if (property === 'animation') return;\n } else if (hasRawTimeInExpression(token.value)) {\n checkDuration(token, property, node);\n if (property === 'animation') return;\n }\n }\n }\n\n function checkValue(\n value: string,\n property: string,\n node: TSESTree.Node,\n ): void {\n let group: ScannedToken[] = [];\n for (const token of scanTokens(value)) {\n if (token.value) group.push(token);\n if (token.isComma) {\n checkGroup(group, property, node);\n group = [];\n }\n }\n checkGroup(group, property, node);\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (!key || !PROPERTIES.has(key)) continue;\n\n const value = getStringValue(prop.value);\n if (value) {\n checkValue(value, key, prop.value);\n } else if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateValue = getStringValue(stateProp.value);\n if (stateValue) checkValue(stateValue, key, stateProp.value);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;;AAiBA,MAAM,aAAa,IAAI,IAAI;CACzB;CACA;CACA;CACD,CAAC;AAEF,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,4EACH;EACD,UAAU;GACR,mBACE;GACF,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,iBAAiB,kBAAkB,IAAI,OAAO,OAAO;EAE3D,SAAS,cACP,OACA,UACA,MACM;GACN,MAAM,QAAQ,MAAM;AACpB,OAAI,CAAC,cAAc,MAAM,IAAI,CAAC,uBAAuB,MAAM,CAAE;GAI7D,MAAM,aACJ,aAAa,uBACT,iBACA,eAAe,QAAQ,SAAS,sBAAsB,KAAK,KAAK,CAAC;GACvE,MAAM,YAAY,cAAc,MAAM;AACtC,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAU,UAAU;KAAO;IACnC,SAAS,YACL,WAAW,KAAK,eAAe;KAC7B,WAAW;KACX,MAAM;MAAE,UAAU;MAAO,OAAO;MAAW;KAC3C,MAAM,UACJ,qBACE,OACA,MACA,CACE;MACE,OAAO,MAAM;MACb,KAAK,MAAM,SAAS,MAAM;MAC1B,aAAa;MACd,CACF,EACD,QAAQ,WACT;KACJ,EAAE,GACH,EAAE;IACP,CAAC;;EAGJ,SAAS,WACP,OACA,UACA,MACM;AACN,QAAK,MAAM,SAAS,OAAO;AAIzB,QACE,aAAa,gBACZ,aAAa,KAAK,MAAM,MAAM,IAC7B,UAAU,KAAK,MAAM,MAAM,IAC3B,WAAW,MAAM,MAAM,IACtB,iBAAiB,MAAM,MAAM,IAC5B,CAAC,uBAAuB,MAAM,MAAM,EAExC;AAEF,QAAI,cAAc,MAAM,MAAM,EAAE;AAC9B,mBAAc,OAAO,UAAU,KAAK;AACpC,SAAI,aAAa,YAAa;eACrB,uBAAuB,MAAM,MAAM,EAAE;AAC9C,mBAAc,OAAO,UAAU,KAAK;AACpC,SAAI,aAAa,YAAa;;;;EAKpC,SAAS,WACP,OACA,UACA,MACM;GACN,IAAI,QAAwB,EAAE;AAC9B,QAAK,MAAM,SAAS,WAAW,MAAM,EAAE;AACrC,QAAI,MAAM,MAAO,OAAM,KAAK,MAAM;AAClC,QAAI,MAAM,SAAS;AACjB,gBAAW,OAAO,UAAU,KAAK;AACjC,aAAQ,EAAE;;;AAGd,cAAW,OAAO,UAAU,KAAK;;EAGnC,SAAS,kBAAkB,MAAuC;AAChE,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,CAAC,OAAO,CAAC,WAAW,IAAI,IAAI,CAAE;IAElC,MAAM,QAAQ,eAAe,KAAK,MAAM;AACxC,QAAI,MACF,YAAW,OAAO,KAAK,KAAK,MAAM;aAElC,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,aAAa,eAAe,UAAU,MAAM;AAClD,SAAI,WAAY,YAAW,YAAY,KAAK,UAAU,MAAM;;;;AAMpE,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
|
+
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import { scanTokens } from "../parsers/utils.js";
|
|
5
|
+
import { replaceInStringValue } from "../fix-utils.js";
|
|
6
|
+
import { getDurationTokens, hasRawTimeInExpression, isNonzeroTime, isTimeLiteral } from "./motion-duration-utils.js";
|
|
7
|
+
|
|
8
|
+
//#region src/rules/no-raw-transition-duration.ts
|
|
9
|
+
/**
|
|
10
|
+
* A plausible transition name: a semantic name, a CSS property, or a `$$name` /
|
|
11
|
+
* `##name` / `--name` custom-property reference.
|
|
12
|
+
*
|
|
13
|
+
* This excludes malformed groups from duration advice.
|
|
14
|
+
*/
|
|
15
|
+
const TRANSITION_NAME = /^(?:\$\$|##|--)?[a-zA-Z][\w-]*$/;
|
|
16
|
+
const NON_TRANSITION_NAMES = new Set([
|
|
17
|
+
"none",
|
|
18
|
+
"inherit",
|
|
19
|
+
"initial",
|
|
20
|
+
"unset",
|
|
21
|
+
"revert",
|
|
22
|
+
"revert-layer"
|
|
23
|
+
]);
|
|
24
|
+
/** The default timing tasty substitutes for a group whose duration is omitted. */
|
|
25
|
+
function defaultTimingHint(name) {
|
|
26
|
+
return `$${name.startsWith("$$") ? name.slice(2) : name.startsWith("##") ? `${name.slice(2)}-color` : name.startsWith("--") ? name.slice(2) : name}-transition (falling back to $transition)`;
|
|
27
|
+
}
|
|
28
|
+
var no_raw_transition_duration_default = createRule({
|
|
29
|
+
name: "no-raw-transition-duration",
|
|
30
|
+
meta: {
|
|
31
|
+
type: "suggestion",
|
|
32
|
+
hasSuggestions: true,
|
|
33
|
+
docs: { description: "Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration" },
|
|
34
|
+
messages: {
|
|
35
|
+
rawTransitionDuration: "Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}.",
|
|
36
|
+
rawTransitionDurationExpression: "Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead.",
|
|
37
|
+
useDurationToken: "Replace '{{duration}}' with '{{token}}'",
|
|
38
|
+
useDefaultDuration: "Remove '{{duration}}' to inherit the default transition timing"
|
|
39
|
+
},
|
|
40
|
+
schema: []
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: [],
|
|
43
|
+
create(context) {
|
|
44
|
+
const ctx = new TastyContext(context);
|
|
45
|
+
/** `$`-prefixed config tokens whose name reads as a duration. */
|
|
46
|
+
const durationTokens = getDurationTokens(ctx.config.tokens);
|
|
47
|
+
function checkGroup(tokens, node) {
|
|
48
|
+
const [name, timing] = tokens;
|
|
49
|
+
if (!name || !timing || !TRANSITION_NAME.test(name.value)) return;
|
|
50
|
+
if (NON_TRANSITION_NAMES.has(name.value)) return;
|
|
51
|
+
const isLiteral = isTimeLiteral(timing.value);
|
|
52
|
+
if (isLiteral && !isNonzeroTime(timing.value)) return;
|
|
53
|
+
if (!isLiteral && !hasRawTimeInExpression(timing.value)) return;
|
|
54
|
+
if (!isLiteral) {
|
|
55
|
+
context.report({
|
|
56
|
+
node,
|
|
57
|
+
messageId: "rawTransitionDurationExpression",
|
|
58
|
+
data: { duration: timing.value }
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const start = timing.offset;
|
|
63
|
+
const end = start + timing.value.length;
|
|
64
|
+
const removeStart = name.offset + name.value.length;
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: "rawTransitionDuration",
|
|
68
|
+
data: {
|
|
69
|
+
duration: timing.value,
|
|
70
|
+
available: durationTokens.length ? ` (${durationTokens.join(", ")})` : "",
|
|
71
|
+
fallback: defaultTimingHint(name.value)
|
|
72
|
+
},
|
|
73
|
+
suggest: [...durationTokens.map((token) => ({
|
|
74
|
+
messageId: "useDurationToken",
|
|
75
|
+
data: {
|
|
76
|
+
duration: timing.value,
|
|
77
|
+
token
|
|
78
|
+
},
|
|
79
|
+
fix: (fixer) => replaceInStringValue(fixer, node, [{
|
|
80
|
+
start,
|
|
81
|
+
end,
|
|
82
|
+
replacement: token
|
|
83
|
+
}], context.sourceCode)
|
|
84
|
+
})), {
|
|
85
|
+
messageId: "useDefaultDuration",
|
|
86
|
+
data: { duration: timing.value },
|
|
87
|
+
fix: (fixer) => replaceInStringValue(fixer, node, [{
|
|
88
|
+
start: removeStart,
|
|
89
|
+
end,
|
|
90
|
+
replacement: ""
|
|
91
|
+
}], context.sourceCode)
|
|
92
|
+
}]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function checkTransitionValue(value, node) {
|
|
96
|
+
let group = [];
|
|
97
|
+
for (const token of scanTokens(value)) {
|
|
98
|
+
if (token.value) group.push(token);
|
|
99
|
+
if (token.isComma) {
|
|
100
|
+
checkGroup(group, node);
|
|
101
|
+
group = [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
checkGroup(group, node);
|
|
105
|
+
}
|
|
106
|
+
function isLocalTastyComponent(node, name) {
|
|
107
|
+
let scope = context.sourceCode.getScope(node);
|
|
108
|
+
while (scope) {
|
|
109
|
+
const variable = scope.set.get(name);
|
|
110
|
+
if (variable) return variable.defs.some((definition) => {
|
|
111
|
+
if (definition.type !== "Variable") return false;
|
|
112
|
+
const declaration = definition.node;
|
|
113
|
+
return declaration.id.type === "Identifier" && declaration.parent.type === "VariableDeclaration" && declaration.parent.kind === "const" && declaration.init?.type === "CallExpression" && ctx.isTastyCall(declaration.init)?.importedName === "tasty";
|
|
114
|
+
});
|
|
115
|
+
scope = scope.upper;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
function handleStyleObject(node) {
|
|
120
|
+
if (!ctx.isStyleObject(node)) return;
|
|
121
|
+
for (const prop of node.properties) {
|
|
122
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
123
|
+
if (getKeyName(prop.key) !== "transition") continue;
|
|
124
|
+
const str = getStringValue(prop.value);
|
|
125
|
+
if (str) {
|
|
126
|
+
checkTransitionValue(str, prop.value);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
|
|
130
|
+
if (stateProp.type !== "Property") continue;
|
|
131
|
+
const stateStr = getStringValue(stateProp.value);
|
|
132
|
+
if (stateStr) checkTransitionValue(stateStr, stateProp.value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ImportDeclaration(node) {
|
|
138
|
+
ctx.trackImport(node);
|
|
139
|
+
},
|
|
140
|
+
JSXAttribute(node) {
|
|
141
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "transition") return;
|
|
142
|
+
const opening = node.parent;
|
|
143
|
+
if (opening?.type !== "JSXOpeningElement" || opening.name.type !== "JSXIdentifier" || !isLocalTastyComponent(node, opening.name.name)) return;
|
|
144
|
+
const valueNode = node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value;
|
|
145
|
+
if (!valueNode) return;
|
|
146
|
+
const value = getStringValue(valueNode);
|
|
147
|
+
if (value) checkTransitionValue(value, valueNode);
|
|
148
|
+
},
|
|
149
|
+
...styleObjectListeners(handleStyleObject)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { no_raw_transition_duration_default as default };
|
|
156
|
+
//# sourceMappingURL=no-raw-transition-duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-raw-transition-duration.js","names":[],"sources":["../../src/rules/no-raw-transition-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanTokens, type ScannedToken } from '../parsers/utils.js';\nimport {\n getDurationTokens,\n hasRawTimeInExpression,\n isNonzeroTime,\n isTimeLiteral,\n} from './motion-duration-utils.js';\n\ntype MessageIds =\n | 'rawTransitionDuration'\n | 'rawTransitionDurationExpression'\n | 'useDurationToken'\n | 'useDefaultDuration';\n\n/**\n * A plausible transition name: a semantic name, a CSS property, or a `$$name` /\n * `##name` / `--name` custom-property reference.\n *\n * This excludes malformed groups from duration advice.\n */\nconst TRANSITION_NAME = /^(?:\\$\\$|##|--)?[a-zA-Z][\\w-]*$/;\nconst NON_TRANSITION_NAMES = new Set([\n 'none',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/** The default timing tasty substitutes for a group whose duration is omitted. */\nfunction defaultTimingHint(name: string): string {\n // The parser turns $$name into --name and ##name into --name-color before\n // the transition handler selects its per-property timing variable.\n const property = name.startsWith('$$')\n ? name.slice(2)\n : name.startsWith('##')\n ? `${name.slice(2)}-color`\n : name.startsWith('--')\n ? name.slice(2)\n : name;\n return `$${property}-transition (falling back to $transition)`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-transition-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n \"Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration\",\n },\n messages: {\n rawTransitionDuration:\n \"Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}.\",\n rawTransitionDurationExpression:\n \"Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n useDefaultDuration:\n \"Remove '{{duration}}' to inherit the default transition timing\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /** `$`-prefixed config tokens whose name reads as a duration. */\n const durationTokens = getDurationTokens(ctx.config.tokens);\n\n function checkGroup(tokens: ScannedToken[], node: TSESTree.Node): void {\n const [name, timing] = tokens;\n\n // Only the second token is the duration slot. A later time value is a\n // delay, for which removing the value has different semantics.\n if (!name || !timing || !TRANSITION_NAME.test(name.value)) return;\n if (NON_TRANSITION_NAMES.has(name.value)) return;\n\n const isLiteral = isTimeLiteral(timing.value);\n if (isLiteral && !isNonzeroTime(timing.value)) return;\n if (!isLiteral && !hasRawTimeInExpression(timing.value)) return;\n\n if (!isLiteral) {\n // Replacing or dropping a whole expression can change its behavior in\n // ways the rule cannot infer, so offer a report without a quick fix.\n context.report({\n node,\n messageId: 'rawTransitionDurationExpression',\n data: { duration: timing.value },\n });\n return;\n }\n\n const start = timing.offset;\n const end = start + timing.value.length;\n // Removal swallows the separating whitespace too, so dropping the\n // duration does not leave `fill ease-in` behind.\n const removeStart = name.offset + name.value.length;\n\n context.report({\n node,\n messageId: 'rawTransitionDuration',\n data: {\n duration: timing.value,\n available: durationTokens.length\n ? ` (${durationTokens.join(', ')})`\n : '',\n fallback: defaultTimingHint(name.value),\n },\n suggest: [\n // Config tokens first: naming the project's own duration is better\n // advice than deleting the author's intent, when one exists.\n ...durationTokens.map((token) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: timing.value, token },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: token }],\n context.sourceCode,\n ),\n })),\n {\n messageId: 'useDefaultDuration' as const,\n data: { duration: timing.value },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start: removeStart, end, replacement: '' }],\n context.sourceCode,\n ),\n },\n ],\n });\n }\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n let group: ScannedToken[] = [];\n for (const token of scanTokens(value)) {\n if (token.value) group.push(token);\n if (token.isComma) {\n checkGroup(group, node);\n group = [];\n }\n }\n checkGroup(group, node);\n }\n\n function isLocalTastyComponent(node: TSESTree.Node, name: string): boolean {\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope) {\n const variable = scope.set.get(name);\n if (variable) {\n return variable.defs.some((definition) => {\n if (definition.type !== 'Variable') return false;\n const declaration = definition.node;\n return (\n declaration.id.type === 'Identifier' &&\n declaration.parent.type === 'VariableDeclaration' &&\n declaration.parent.kind === 'const' &&\n declaration.init?.type === 'CallExpression' &&\n ctx.isTastyCall(declaration.init)?.importedName === 'tasty'\n );\n });\n }\n scope = scope.upper;\n }\n return false;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(str, prop.value);\n continue;\n }\n\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkTransitionValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n JSXAttribute(node) {\n if (\n node.name.type !== 'JSXIdentifier' ||\n node.name.name !== 'transition'\n ) {\n return;\n }\n const opening = node.parent;\n if (\n opening?.type !== 'JSXOpeningElement' ||\n opening.name.type !== 'JSXIdentifier' ||\n !isLocalTastyComponent(node, opening.name.name)\n ) {\n return;\n }\n\n const valueNode =\n node.value?.type === 'JSXExpressionContainer'\n ? node.value.expression\n : node.value;\n if (!valueNode) return;\n const value = getStringValue(valueNode);\n if (value) checkTransitionValue(value, valueNode);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;;;;;;;;AAyBA,MAAM,kBAAkB;AACxB,MAAM,uBAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,SAAS,kBAAkB,MAAsB;AAU/C,QAAO,IAPU,KAAK,WAAW,KAAK,GAClC,KAAK,MAAM,EAAE,GACb,KAAK,WAAW,KAAK,GACnB,GAAG,KAAK,MAAM,EAAE,CAAC,UACjB,KAAK,WAAW,KAAK,GACnB,KAAK,MAAM,EAAE,GACb,KACY;;AAGtB,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,mGACH;EACD,UAAU;GACR,uBACE;GACF,iCACE;GACF,kBAAkB;GAClB,oBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;;EAGrC,MAAM,iBAAiB,kBAAkB,IAAI,OAAO,OAAO;EAE3D,SAAS,WAAW,QAAwB,MAA2B;GACrE,MAAM,CAAC,MAAM,UAAU;AAIvB,OAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,gBAAgB,KAAK,KAAK,MAAM,CAAE;AAC3D,OAAI,qBAAqB,IAAI,KAAK,MAAM,CAAE;GAE1C,MAAM,YAAY,cAAc,OAAO,MAAM;AAC7C,OAAI,aAAa,CAAC,cAAc,OAAO,MAAM,CAAE;AAC/C,OAAI,CAAC,aAAa,CAAC,uBAAuB,OAAO,MAAM,CAAE;AAEzD,OAAI,CAAC,WAAW;AAGd,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,UAAU,OAAO,OAAO;KACjC,CAAC;AACF;;GAGF,MAAM,QAAQ,OAAO;GACrB,MAAM,MAAM,QAAQ,OAAO,MAAM;GAGjC,MAAM,cAAc,KAAK,SAAS,KAAK,MAAM;AAE7C,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KACJ,UAAU,OAAO;KACjB,WAAW,eAAe,SACtB,KAAK,eAAe,KAAK,KAAK,CAAC,KAC/B;KACJ,UAAU,kBAAkB,KAAK,MAAM;KACxC;IACD,SAAS,CAGP,GAAG,eAAe,KAAK,WAAW;KAChC,WAAW;KACX,MAAM;MAAE,UAAU,OAAO;MAAO;MAAO;KACvC,MAAM,UACJ,qBACE,OACA,MACA,CAAC;MAAE;MAAO;MAAK,aAAa;MAAO,CAAC,EACpC,QAAQ,WACT;KACJ,EAAE,EACH;KACE,WAAW;KACX,MAAM,EAAE,UAAU,OAAO,OAAO;KAChC,MAAM,UACJ,qBACE,OACA,MACA,CAAC;MAAE,OAAO;MAAa;MAAK,aAAa;MAAI,CAAC,EAC9C,QAAQ,WACT;KACJ,CACF;IACF,CAAC;;EAGJ,SAAS,qBAAqB,OAAe,MAA2B;GACtE,IAAI,QAAwB,EAAE;AAC9B,QAAK,MAAM,SAAS,WAAW,MAAM,EAAE;AACrC,QAAI,MAAM,MAAO,OAAM,KAAK,MAAM;AAClC,QAAI,MAAM,SAAS;AACjB,gBAAW,OAAO,KAAK;AACvB,aAAQ,EAAE;;;AAGd,cAAW,OAAO,KAAK;;EAGzB,SAAS,sBAAsB,MAAqB,MAAuB;GACzE,IAAI,QACF,QAAQ,WAAW,SAAS,KAAK;AACnC,UAAO,OAAO;IACZ,MAAM,WAAW,MAAM,IAAI,IAAI,KAAK;AACpC,QAAI,SACF,QAAO,SAAS,KAAK,MAAM,eAAe;AACxC,SAAI,WAAW,SAAS,WAAY,QAAO;KAC3C,MAAM,cAAc,WAAW;AAC/B,YACE,YAAY,GAAG,SAAS,gBACxB,YAAY,OAAO,SAAS,yBAC5B,YAAY,OAAO,SAAS,WAC5B,YAAY,MAAM,SAAS,oBAC3B,IAAI,YAAY,YAAY,KAAK,EAAE,iBAAiB;MAEtD;AAEJ,YAAQ,MAAM;;AAEhB,UAAO;;EAGT,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,QADY,WAAW,KAAK,IAAI,KACpB,aAAc;IAE1B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,0BAAqB,KAAK,KAAK,MAAM;AACrC;;AAGF,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,sBAAqB,UAAU,UAAU,MAAM;;;;AAOzD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,aAAa,MAAM;AACjB,QACE,KAAK,KAAK,SAAS,mBACnB,KAAK,KAAK,SAAS,aAEnB;IAEF,MAAM,UAAU,KAAK;AACrB,QACE,SAAS,SAAS,uBAClB,QAAQ,KAAK,SAAS,mBACtB,CAAC,sBAAsB,MAAM,QAAQ,KAAK,KAAK,CAE/C;IAGF,MAAM,YACJ,KAAK,OAAO,SAAS,2BACjB,KAAK,MAAM,aACX,KAAK;AACX,QAAI,CAAC,UAAW;IAChB,MAAM,QAAQ,eAAe,UAAU;AACvC,QAAI,MAAO,sBAAqB,OAAO,UAAU;;GAEnD,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import { collectLocalStateAliases,
|
|
2
|
+
import { collectLocalStateAliases, getKeyName, getStringValue, isKnownStateAlias } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
4
|
|
|
5
5
|
//#region src/rules/no-unknown-state-alias.ts
|
|
@@ -28,7 +28,7 @@ var no_unknown_state_alias_default = createRule({
|
|
|
28
28
|
}
|
|
29
29
|
function handleStyleObject(node) {
|
|
30
30
|
if (!ctx.isStyleObject(node)) return;
|
|
31
|
-
const localAliases = collectLocalStateAliases(
|
|
31
|
+
const localAliases = collectLocalStateAliases(ctx.getRootStyleObject(node));
|
|
32
32
|
if (ctx.config.states.length === 0 && localAliases.length === 0) return;
|
|
33
33
|
for (const prop of node.properties) {
|
|
34
34
|
if (prop.type !== "Property" || prop.computed) continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unknown-state-alias.js","names":[],"sources":["../../src/rules/no-unknown-state-alias.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n getKeyName,\n getStringValue,\n isKnownStateAlias,\n collectLocalStateAliases,\n
|
|
1
|
+
{"version":3,"file":"no-unknown-state-alias.js","names":[],"sources":["../../src/rules/no-unknown-state-alias.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n getKeyName,\n getStringValue,\n isKnownStateAlias,\n collectLocalStateAliases,\n} from '../utils.js';\n\ntype MessageIds = 'unknownAlias';\n\nexport default createRule<[], MessageIds>({\n name: 'no-unknown-state-alias',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n \"Warn when a @name state alias is used that isn't in the config\",\n },\n messages: {\n unknownAlias:\n \"Unknown state alias '{{alias}}'. Add it to 'states' in your tasty config, or define it locally as a '@name' key.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkStateKeys(\n obj: TSESTree.ObjectExpression,\n localAliases: string[],\n ): void {\n for (const prop of obj.properties) {\n if (prop.type !== 'Property') continue;\n\n const key = !prop.computed\n ? getKeyName(prop.key)\n : getStringValue(prop.key);\n if (key === null || !key.startsWith('@')) continue;\n\n if (\n !isKnownStateAlias(key, ctx.config) &&\n !localAliases.includes(key)\n ) {\n context.report({\n node: prop.key,\n messageId: 'unknownAlias',\n data: { alias: key },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const rootObj = ctx.getRootStyleObject(node);\n const localAliases = collectLocalStateAliases(rootObj);\n\n // Skip if no states configured and no local aliases\n if (ctx.config.states.length === 0 && localAliases.length === 0) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n // Skip sub-elements and special keys\n const key = getKeyName(prop.key);\n if (key === null || /^[A-Z@&]/.test(key)) continue;\n\n // Check state map objects for unknown aliases\n if (prop.value.type === 'ObjectExpression') {\n checkStateKeys(prop.value, localAliases);\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAYA,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,kEACH;EACD,UAAU,EACR,cACE,oHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,eACP,KACA,cACM;AACN,QAAK,MAAM,QAAQ,IAAI,YAAY;AACjC,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MAAM,CAAC,KAAK,WACd,WAAW,KAAK,IAAI,GACpB,eAAe,KAAK,IAAI;AAC5B,QAAI,QAAQ,QAAQ,CAAC,IAAI,WAAW,IAAI,CAAE;AAE1C,QACE,CAAC,kBAAkB,KAAK,IAAI,OAAO,IACnC,CAAC,aAAa,SAAS,IAAI,CAE3B,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,OAAO,KAAK;KACrB,CAAC;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;GAG9B,MAAM,eAAe,yBADL,IAAI,mBAAmB,KAAK,CACU;AAGtD,OAAI,IAAI,OAAO,OAAO,WAAW,KAAK,aAAa,WAAW,EAAG;AAEjE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAG/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,QAAQ,WAAW,KAAK,IAAI,CAAE;AAG1C,QAAI,KAAK,MAAM,SAAS,mBACtB,gBAAe,KAAK,OAAO,aAAa;;;AAK9C,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -15,6 +15,16 @@ var valid_boolean_property_default = createRule({
|
|
|
15
15
|
defaultOptions: [],
|
|
16
16
|
create(context) {
|
|
17
17
|
const ctx = new TastyContext(context);
|
|
18
|
+
function report(name, node) {
|
|
19
|
+
context.report({
|
|
20
|
+
node,
|
|
21
|
+
messageId: "invalidBooleanTrue",
|
|
22
|
+
data: {
|
|
23
|
+
name,
|
|
24
|
+
allowed: [...BOOLEAN_TRUE_PROPERTIES].join(", ")
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
18
28
|
function handleStyleObject(node) {
|
|
19
29
|
if (!ctx.isStyleObject(node)) return;
|
|
20
30
|
for (const prop of node.properties) {
|
|
@@ -22,14 +32,15 @@ var valid_boolean_property_default = createRule({
|
|
|
22
32
|
const key = getKeyName(prop.key);
|
|
23
33
|
if (key === null) continue;
|
|
24
34
|
if (/^[A-Z@&$#]/.test(key)) continue;
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
if (BOOLEAN_TRUE_PROPERTIES.has(key)) continue;
|
|
36
|
+
if (prop.value.type === "Literal" && prop.value.value === true) {
|
|
37
|
+
report(key, prop.value);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
|
|
41
|
+
if (stateProp.type !== "Property") continue;
|
|
42
|
+
if (stateProp.value.type === "Literal" && stateProp.value.value === true) report(key, stateProp.value);
|
|
43
|
+
}
|
|
33
44
|
}
|
|
34
45
|
}
|
|
35
46
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-boolean-property.js","names":[],"sources":["../../src/rules/valid-boolean-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName } from '../utils.js';\nimport { BOOLEAN_TRUE_PROPERTIES } from '../constants.js';\n\ntype MessageIds = 'invalidBooleanTrue';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-boolean-property',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate that true/false values are only used on properties that support them',\n },\n messages: {\n invalidBooleanTrue:\n \"Property '{{name}}' does not accept boolean true. Only these properties support it: {{allowed}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n // Skip sub-elements and special keys\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n // Check for true literal — false is always valid (tombstone)\n if (
|
|
1
|
+
{"version":3,"file":"valid-boolean-property.js","names":[],"sources":["../../src/rules/valid-boolean-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName } from '../utils.js';\nimport { BOOLEAN_TRUE_PROPERTIES } from '../constants.js';\n\ntype MessageIds = 'invalidBooleanTrue';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-boolean-property',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate that true/false values are only used on properties that support them',\n },\n messages: {\n invalidBooleanTrue:\n \"Property '{{name}}' does not accept boolean true. Only these properties support it: {{allowed}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function report(name: string, node: TSESTree.Node): void {\n context.report({\n node,\n messageId: 'invalidBooleanTrue',\n data: { name, allowed: [...BOOLEAN_TRUE_PROPERTIES].join(', ') },\n });\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n // Skip sub-elements and special keys\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n if (BOOLEAN_TRUE_PROPERTIES.has(key)) continue;\n\n // Check for true literal — false is always valid (tombstone)\n if (prop.value.type === 'Literal' && prop.value.value === true) {\n report(key, prop.value);\n continue;\n }\n\n // A state map holds values for the same property, so `true` is exactly\n // as unsupported in one as it is written directly — and rather more\n // likely, since a per-state default (`fill: { '': '#clear', hovered:\n // true }`) is the shape that reads as if it should work.\n //\n // Only genuine state maps: a sub-element object is a style object in its\n // own right and is visited by the listener, so descending into one here\n // would attribute its keys to the outer property.\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n if (\n stateProp.value.type === 'Literal' &&\n stateProp.value.value === true\n ) {\n report(key, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iFACH;EACD,UAAU,EACR,oBACE,oGACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,OAAO,MAAc,MAA2B;AACvD,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAM,SAAS,CAAC,GAAG,wBAAwB,CAAC,KAAK,KAAK;KAAE;IACjE,CAAC;;EAGJ,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAGlB,QAAI,aAAa,KAAK,IAAI,CAAE;AAE5B,QAAI,wBAAwB,IAAI,IAAI,CAAE;AAGtC,QAAI,KAAK,MAAM,SAAS,aAAa,KAAK,MAAM,UAAU,MAAM;AAC9D,YAAO,KAAK,KAAK,MAAM;AACvB;;AAWF,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;AACnC,SACE,UAAU,MAAM,SAAS,aACzB,UAAU,MAAM,UAAU,KAE1B,QAAO,KAAK,UAAU,MAAM;;;;AAOtC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -5,6 +5,19 @@ import { TastyContext, styleObjectListeners } from "../context.js";
|
|
|
5
5
|
import { parseValue } from "../parsers/value-parser.js";
|
|
6
6
|
|
|
7
7
|
//#region src/rules/valid-directional-modifier.ts
|
|
8
|
+
/**
|
|
9
|
+
* Every direction word the rule recognises, across physical and logical
|
|
10
|
+
* vocabularies. A word here is read as a *modifier*; whether it is accepted is
|
|
11
|
+
* then decided per property by `DIRECTIONAL_MODIFIERS`.
|
|
12
|
+
*
|
|
13
|
+
* `start` and `end` are ordinary CSS *values* for a long tail of properties
|
|
14
|
+
* (`textAlign: 'start'`, `justifyContent: 'end'`), which is exactly what the
|
|
15
|
+
* `DIRECTIONAL_MODIFIERS` gate in `checkValue` exists to keep out: only the
|
|
16
|
+
* properties with a directional vocabulary are ever inspected. Inside that set
|
|
17
|
+
* no property takes `start`/`end` as a value, so reading them as modifiers is
|
|
18
|
+
* safe — and it is what turns `padding: '1x start'` (a physical property given
|
|
19
|
+
* a logical edge, which the handler drops) into a report.
|
|
20
|
+
*/
|
|
8
21
|
const ALL_DIRECTIONS = new Set([
|
|
9
22
|
"top",
|
|
10
23
|
"right",
|
|
@@ -13,7 +26,9 @@ const ALL_DIRECTIONS = new Set([
|
|
|
13
26
|
"top-left",
|
|
14
27
|
"top-right",
|
|
15
28
|
"bottom-left",
|
|
16
|
-
"bottom-right"
|
|
29
|
+
"bottom-right",
|
|
30
|
+
"start",
|
|
31
|
+
"end"
|
|
17
32
|
]);
|
|
18
33
|
/**
|
|
19
34
|
* Token types that count as a *value* rather than a modifier.
|
|
@@ -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, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.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\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]);\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 function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n // Sub-elements, at-rules, token declarations, nested selectors, and the\n // `$` affix (which holds a selector, not a value) are not style values.\n if (\n key === '$' ||\n key.startsWith('@') ||\n key.startsWith('$') ||\n key.startsWith('#') ||\n key.startsWith('&') ||\n /^[A-Z]/.test(key)\n ) {\n continue;\n }\n\n // Direct value\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n // State map\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;AAcA,MAAM,iBAAiB,IAAI,IAAI;CAC7B;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;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAIlB,QACE,QAAQ,OACR,IAAI,WAAW,IAAI,IACnB,IAAI,WAAW,IAAI,IACnB,IAAI,WAAW,IAAI,IACnB,IAAI,WAAW,IAAI,IACnB,SAAS,KAAK,IAAI,CAElB;IAIF,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;AAChC;;AAIF,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,YAAW,KAAK,UAAU,UAAU,MAAM;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,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, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.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 function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n // Sub-elements, at-rules, token declarations, nested selectors, and the\n // `$` affix (which holds a selector, not a value) are not style values.\n if (\n key === '$' ||\n key.startsWith('@') ||\n key.startsWith('$') ||\n key.startsWith('#') ||\n key.startsWith('&') ||\n /^[A-Z]/.test(key)\n ) {\n continue;\n }\n\n // Direct value\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n // State map\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import { collectLocalStateAliases,
|
|
2
|
+
import { collectLocalStateAliases, getKeyName, getStringValue } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
4
|
import { parseStateKey } from "../parsers/state-key-parser.js";
|
|
5
5
|
|
|
@@ -19,15 +19,7 @@ var valid_state_key_default = createRule({
|
|
|
19
19
|
create(context) {
|
|
20
20
|
const ctx = new TastyContext(context);
|
|
21
21
|
const parserOpts = { knownAliases: ctx.config.states };
|
|
22
|
-
function
|
|
23
|
-
let current = node.parent;
|
|
24
|
-
while (current) {
|
|
25
|
-
if (current.type === "Property" && !current.computed && current.key.type === "Identifier" && /^[A-Z]/.test(current.key.name)) return true;
|
|
26
|
-
current = current.parent;
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
function checkStateKey(key, keyNode, insideSubElement, localAliases) {
|
|
22
|
+
function checkStateKey(key, keyNode, localAliases) {
|
|
31
23
|
if (key === "") return;
|
|
32
24
|
const result = parseStateKey(key, parserOpts);
|
|
33
25
|
for (const error of result.errors) context.report({
|
|
@@ -49,8 +41,7 @@ var valid_state_key_default = createRule({
|
|
|
49
41
|
}
|
|
50
42
|
function handleStyleObject(node) {
|
|
51
43
|
if (!ctx.isStyleObject(node)) return;
|
|
52
|
-
const
|
|
53
|
-
const localAliases = collectLocalStateAliases(findRootStyleObject(node));
|
|
44
|
+
const localAliases = collectLocalStateAliases(ctx.getRootStyleObject(node));
|
|
54
45
|
for (const prop of node.properties) {
|
|
55
46
|
if (prop.type !== "Property" || prop.computed) continue;
|
|
56
47
|
const key = getKeyName(prop.key);
|
|
@@ -61,7 +52,7 @@ var valid_state_key_default = createRule({
|
|
|
61
52
|
if (stateProp.type !== "Property") continue;
|
|
62
53
|
const stateKey = !stateProp.computed ? getKeyName(stateProp.key) : getStringValue(stateProp.key);
|
|
63
54
|
if (stateKey === null) continue;
|
|
64
|
-
checkStateKey(stateKey, stateProp.key,
|
|
55
|
+
checkStateKey(stateKey, stateProp.key, localAliases);
|
|
65
56
|
}
|
|
66
57
|
}
|
|
67
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-state-key.js","names":[],"sources":["../../src/rules/valid-state-key.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n getKeyName,\n getStringValue,\n collectLocalStateAliases,\n
|
|
1
|
+
{"version":3,"file":"valid-state-key.js","names":[],"sources":["../../src/rules/valid-state-key.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n getKeyName,\n getStringValue,\n collectLocalStateAliases,\n} from '../utils.js';\nimport { parseStateKey } from '../parsers/state-key-parser.js';\nimport type { StateKeyParserOptions } from '../parsers/state-key-parser.js';\n\ntype MessageIds = 'invalidStateKey' | 'unknownAlias';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-state-key',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate state key syntax in style mapping objects',\n },\n messages: {\n invalidStateKey: '{{reason}}',\n unknownAlias:\n \"Unknown state alias '{{alias}}'. Configured aliases: {{known}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n const parserOpts: StateKeyParserOptions = {\n knownAliases: ctx.config.states,\n };\n\n function checkStateKey(\n key: string,\n keyNode: TSESTree.Node,\n localAliases: string[],\n ): void {\n if (key === '') return;\n\n const result = parseStateKey(key, parserOpts);\n\n // Report all parse/validation errors\n for (const error of result.errors) {\n context.report({\n node: keyNode,\n messageId: 'invalidStateKey',\n data: { reason: error.message },\n });\n }\n\n // Check aliases against config and local definitions\n const allKnown = [...ctx.config.states, ...localAliases];\n if (allKnown.length > 0) {\n for (const alias of result.referencedAliases) {\n if (!allKnown.includes(alias)) {\n context.report({\n node: keyNode,\n messageId: 'unknownAlias',\n data: {\n alias,\n known: allKnown.join(', '),\n },\n });\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const rootObj = ctx.getRootStyleObject(node);\n const localAliases = collectLocalStateAliases(rootObj);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null) continue;\n\n checkStateKey(stateKey, stateProp.key, localAliases);\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAaA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,sDACd;EACD,UAAU;GACR,iBAAiB;GACjB,cACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,MAAM,aAAoC,EACxC,cAAc,IAAI,OAAO,QAC1B;EAED,SAAS,cACP,KACA,SACA,cACM;AACN,OAAI,QAAQ,GAAI;GAEhB,MAAM,SAAS,cAAc,KAAK,WAAW;AAG7C,QAAK,MAAM,SAAS,OAAO,OACzB,SAAQ,OAAO;IACb,MAAM;IACN,WAAW;IACX,MAAM,EAAE,QAAQ,MAAM,SAAS;IAChC,CAAC;GAIJ,MAAM,WAAW,CAAC,GAAG,IAAI,OAAO,QAAQ,GAAG,aAAa;AACxD,OAAI,SAAS,SAAS,GACpB;SAAK,MAAM,SAAS,OAAO,kBACzB,KAAI,CAAC,SAAS,SAAS,MAAM,CAC3B,SAAQ,OAAO;KACb,MAAM;KACN,WAAW;KACX,MAAM;MACJ;MACA,OAAO,SAAS,KAAK,KAAK;MAC3B;KACF,CAAC;;;EAMV,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;GAG9B,MAAM,eAAe,yBADL,IAAI,mBAAmB,KAAK,CACU;AAEtD,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAElB,QAAI,SAAS,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,CAClE;AAEF,QAAI,KAAK,MAAM,SAAS,mBAAoB;AAE5C,SAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KAEnC,MAAM,WAAW,CAAC,UAAU,WACxB,WAAW,UAAU,IAAI,GACzB,eAAe,UAAU,IAAI;AACjC,SAAI,aAAa,KAAM;AAEvB,mBAAc,UAAU,UAAU,KAAK,aAAa;;;;AAK1D,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import { getKeyName } from "../utils.js";
|
|
2
|
+
import { getKeyName, unwrapExpression } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
4
|
|
|
5
5
|
//#region src/rules/valid-sub-element.ts
|
|
@@ -20,9 +20,10 @@ var valid_sub_element_default = createRule({
|
|
|
20
20
|
if (prop.type !== "Property" || prop.computed) continue;
|
|
21
21
|
const key = getKeyName(prop.key);
|
|
22
22
|
if (key === null || !/^[A-Z]/.test(key)) continue;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const value = unwrapExpression(prop.value);
|
|
24
|
+
if (value.type !== "ObjectExpression") {
|
|
25
|
+
if (value.type === "Literal" && value.value === false) continue;
|
|
26
|
+
const valueType = value.type === "Literal" ? typeof value.value : value.type;
|
|
26
27
|
context.report({
|
|
27
28
|
node: prop.value,
|
|
28
29
|
messageId: "subElementNotObject",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-sub-element.js","names":[],"sources":["../../src/rules/valid-sub-element.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName } from '../utils.js';\n\ntype MessageIds = 'subElementNotObject';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-sub-element',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate sub-element key format in style objects',\n },\n messages: {\n subElementNotObject:\n \"Sub-element '{{name}}' value must be a style object, not a {{type}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null || !/^[A-Z]/.test(key)) continue;\n\n
|
|
1
|
+
{"version":3,"file":"valid-sub-element.js","names":[],"sources":["../../src/rules/valid-sub-element.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'subElementNotObject';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-sub-element',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate sub-element key format in style objects',\n },\n messages: {\n subElementNotObject:\n \"Sub-element '{{name}}' value must be a style object, not a {{type}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null || !/^[A-Z]/.test(key)) continue;\n\n const value = unwrapExpression(prop.value);\n if (value.type !== 'ObjectExpression') {\n if (value.type === 'Literal' && value.value === false) {\n continue;\n }\n\n const valueType =\n value.type === 'Literal' ? typeof value.value : value.type;\n\n context.report({\n node: prop.value,\n messageId: 'subElementNotObject',\n data: { name: key, type: valueType },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,oDACd;EACD,UAAU,EACR,qBACE,wEACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAE;IAEzC,MAAM,QAAQ,iBAAiB,KAAK,MAAM;AAC1C,QAAI,MAAM,SAAS,oBAAoB;AACrC,SAAI,MAAM,SAAS,aAAa,MAAM,UAAU,MAC9C;KAGF,MAAM,YACJ,MAAM,SAAS,YAAY,OAAO,MAAM,QAAQ,MAAM;AAExD,aAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,MAAM;OAAE,MAAM;OAAK,MAAM;OAAW;MACrC,CAAC;;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|