eslint-plugin-tailwind-variants 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +6 -4
  2. package/dist/src/index.d.ts +31 -0
  3. package/dist/src/index.d.ts.map +1 -0
  4. package/dist/src/rules/index.d.ts +7 -0
  5. package/dist/src/rules/index.d.ts.map +1 -0
  6. package/dist/src/rules/limited-inline-classes.d.ts +41 -0
  7. package/dist/src/rules/limited-inline-classes.d.ts.map +1 -0
  8. package/dist/src/rules/require-variants-call-styles-name.d.ts +19 -0
  9. package/dist/src/rules/require-variants-call-styles-name.d.ts.map +1 -0
  10. package/dist/src/rules/require-variants-suffix.d.ts +19 -0
  11. package/dist/src/rules/require-variants-suffix.d.ts.map +1 -0
  12. package/dist/src/rules/sort-custom-properties.d.ts +86 -0
  13. package/dist/src/rules/sort-custom-properties.d.ts.map +1 -0
  14. package/dist/src/utils/create-rule-visitors.d.ts +2 -0
  15. package/dist/src/utils/create-rule-visitors.d.ts.map +1 -0
  16. package/dist/src/utils/get-bind-class-expression.d.ts +3 -0
  17. package/dist/src/utils/get-bind-class-expression.d.ts.map +1 -0
  18. package/package.json +43 -37
  19. package/src/index.js +61 -0
  20. package/{dist → src}/rules/index.js +5 -5
  21. package/src/rules/limited-inline-classes.js +440 -0
  22. package/src/rules/limited-inline-classes.test.js +268 -0
  23. package/src/rules/require-variants-call-styles-name.js +195 -0
  24. package/src/rules/require-variants-call-styles-name.test.js +105 -0
  25. package/src/rules/require-variants-suffix.js +146 -0
  26. package/src/rules/require-variants-suffix.test.js +81 -0
  27. package/src/rules/sort-custom-properties.js +596 -0
  28. package/src/rules/sort-custom-properties.test.js +758 -0
  29. package/src/utils/create-rule-visitors.js +28 -0
  30. package/src/utils/get-bind-class-expression.js +36 -0
  31. package/dist/index.d.ts +0 -11
  32. package/dist/index.js +0 -43
  33. package/dist/rules/index.d.ts +0 -2
  34. package/dist/rules/limited-inline-classes.d.ts +0 -23
  35. package/dist/rules/limited-inline-classes.js +0 -198
  36. package/dist/rules/require-variants-call-styles-name.d.ts +0 -17
  37. package/dist/rules/require-variants-call-styles-name.js +0 -75
  38. package/dist/rules/require-variants-suffix.d.ts +0 -17
  39. package/dist/rules/require-variants-suffix.js +0 -66
  40. package/dist/rules/sort-custom-properties.d.ts +0 -37
  41. package/dist/rules/sort-custom-properties.js +0 -210
  42. package/dist/utils/create-rule-visitors.d.ts +0 -6
  43. package/dist/utils/create-rule-visitors.js +0 -18
  44. package/dist/utils/get-bind-class-expression.d.ts +0 -6
  45. package/dist/utils/get-bind-class-expression.js +0 -20
@@ -0,0 +1,195 @@
1
+ export const MESSAGE_IDS = {
2
+ renameAllOccurrences: "renameAllOccurrences",
3
+ requireVariantsCallStylesName: "requireVariantsCallStylesName",
4
+ };
5
+
6
+ /** @typedef {typeof MESSAGE_IDS[keyof typeof MESSAGE_IDS]} MessageIds */
7
+
8
+ /**
9
+ * @typedef {object} RuleOptions
10
+ * @property {string} [name="styles"] Name required for variables assigned from tv().
11
+ */
12
+
13
+ /** @type {import("eslint").Rule.RuleModule} */
14
+ export const rule = {
15
+ create: (context) => {
16
+ const [options = {}] = /** @type {[RuleOptions]} */ (context.options);
17
+ const requiredName = options.name ?? "styles";
18
+
19
+ // Tracks by name, not scope—shadowed identifiers may cause false positives
20
+ /** @type {Set<string>} */
21
+ const variantFunctions = new Set();
22
+
23
+ /**
24
+ * Check if expression is a tv() call and track the variable name if so.
25
+ * @param {import("estree").CallExpression} init
26
+ * @param {import("estree").Identifier} id
27
+ * @returns {boolean} `true` if expression is a tv() call and variable name is tracked.
28
+ */
29
+ const trackVariantFunction = (init, id) => {
30
+ if (init.callee.type === "Identifier" && init.callee.name === "tv") {
31
+ variantFunctions.add(id.name);
32
+ return true;
33
+ }
34
+ return false;
35
+ };
36
+
37
+ return {
38
+ VariableDeclarator(node) {
39
+ const { id, init } = node;
40
+
41
+ if (
42
+ !init ||
43
+ init.type !== "CallExpression" ||
44
+ init.callee.type !== "Identifier" ||
45
+ id.type !== "Identifier"
46
+ ) {
47
+ return;
48
+ }
49
+
50
+ if (trackVariantFunction(init, id)) {
51
+ return;
52
+ }
53
+
54
+ if (variantFunctions.has(init.callee.name)) {
55
+ detectVariantVariableNameViolation({
56
+ context,
57
+ declaratorNode: node,
58
+ id,
59
+ requiredName,
60
+ });
61
+ }
62
+ },
63
+ };
64
+ },
65
+
66
+ meta: {
67
+ defaultOptions: [
68
+ {
69
+ name: "styles",
70
+ },
71
+ ],
72
+ docs: {
73
+ description:
74
+ "Require variables assigned from calling a function returned by tv() to be named a specific name.",
75
+ },
76
+ hasSuggestions: true,
77
+ messages: {
78
+ renameAllOccurrences: "Rename all occurrences to {{name}}",
79
+ requireVariantsCallStylesName:
80
+ "Require variables assigned from calling a function returned by tv() to be named {{name}}.",
81
+ },
82
+ schema: [
83
+ {
84
+ additionalProperties: false,
85
+ properties: {
86
+ name: {
87
+ default: "styles",
88
+ description: "Name required for variables assigned from tv().",
89
+ type: "string",
90
+ },
91
+ },
92
+ type: "object",
93
+ },
94
+ ],
95
+ type: "suggestion",
96
+ },
97
+ };
98
+
99
+ /**
100
+ * Collect all references to a variable declaration, excluding the initial
101
+ * declaration.
102
+ * @param {object} options
103
+ * @param {import("eslint").SourceCode} options.sourceCode ESLint SourceCode instance.
104
+ * @param {import("estree").VariableDeclarator} options.declaratorNode VariableDeclarator node of the declaration.
105
+ * @param {import("estree").Identifier} options.id Identifier node of the variable being declared.
106
+ * @returns {import("estree").Identifier[]} Array of identifier nodes referencing the variable.
107
+ */
108
+ const collectReferences = (options) => {
109
+ const declaredVariables = options.sourceCode.getDeclaredVariables(
110
+ options.declaratorNode,
111
+ );
112
+
113
+ /** @type {import("estree").Identifier[]} */
114
+ const references = [];
115
+
116
+ for (const variable of declaredVariables) {
117
+ if (variable.name === options.id.name) {
118
+ for (const reference of variable.references) {
119
+ if (
120
+ reference.identifier !== options.id &&
121
+ reference.identifier.type === "Identifier"
122
+ ) {
123
+ references.push(reference.identifier);
124
+ }
125
+ }
126
+ break;
127
+ }
128
+ }
129
+
130
+ return references;
131
+ };
132
+
133
+ /**
134
+ * Report incorrect variable name with autofix suggestion to rename all
135
+ * occurrences.
136
+ * @param {object} options
137
+ * @param {import("eslint").Rule.RuleContext} options.context
138
+ * @param {import("estree").Identifier} options.id Identifier node of the variable declaration.
139
+ * @param {import("estree").VariableDeclarator} options.declaratorNode
140
+ * @param {string} options.requiredName Required variable name.
141
+ */
142
+ const reportIncorrectName = (options) => {
143
+ const { sourceCode } = options.context;
144
+ const references = collectReferences({
145
+ declaratorNode: options.declaratorNode,
146
+ id: options.id,
147
+ sourceCode,
148
+ });
149
+
150
+ options.context.report({
151
+ data: {
152
+ name: options.requiredName,
153
+ },
154
+ messageId: MESSAGE_IDS.requireVariantsCallStylesName,
155
+ node: options.id,
156
+ suggest: [
157
+ {
158
+ data: {
159
+ name: options.requiredName,
160
+ },
161
+ fix: (fixer) => {
162
+ const fixes = [fixer.replaceText(options.id, options.requiredName)];
163
+
164
+ for (const reference of references) {
165
+ fixes.push(fixer.replaceText(reference, options.requiredName));
166
+ }
167
+
168
+ return fixes;
169
+ },
170
+ messageId: MESSAGE_IDS.renameAllOccurrences,
171
+ },
172
+ ],
173
+ });
174
+ };
175
+
176
+ /**
177
+ * Detect variant variable name violations and report if found.
178
+ * @param {object} options
179
+ * @param {import("eslint").Rule.RuleContext} options.context
180
+ * @param {import("estree").Identifier} options.id Identifier node of the variable declaration.
181
+ * @param {import("estree").VariableDeclarator} options.declaratorNode
182
+ * @param {string} options.requiredName Required variable name.
183
+ */
184
+ const detectVariantVariableNameViolation = (options) => {
185
+ const variableName = options.id.name;
186
+
187
+ if (variableName !== options.requiredName) {
188
+ reportIncorrectName({
189
+ context: options.context,
190
+ declaratorNode: options.declaratorNode,
191
+ id: options.id,
192
+ requiredName: options.requiredName,
193
+ });
194
+ }
195
+ };
@@ -0,0 +1,105 @@
1
+ import { RuleTester } from "eslint";
2
+
3
+ import { MESSAGE_IDS, rule } from "./require-variants-call-styles-name";
4
+
5
+ const tester = new RuleTester();
6
+
7
+ /** @type {import("eslint").RuleTester.ValidTestCase[]} */
8
+ const valid = [
9
+ {
10
+ code: `const buttonVariants = tv({})`,
11
+ name: "Direct tv() call - no enforcement",
12
+ },
13
+ {
14
+ code: `
15
+ const buttonVariants = tv({});
16
+ const styles = buttonVariants();
17
+ `,
18
+ name: "Variant function call named 'styles' (default)",
19
+ },
20
+ {
21
+ code: `
22
+ const cardVariants = tv({});
23
+ const variants = cardVariants();
24
+ `,
25
+ name: "Variable named custom 'variants'",
26
+ options: [{ name: "variants" }],
27
+ },
28
+ ];
29
+
30
+ /** @type {import("eslint").RuleTester.InvalidTestCase[]} */
31
+ const invalid = [
32
+ {
33
+ code: `
34
+ const buttonVariants = tv({});
35
+ const buttonStyles = buttonVariants();
36
+ `,
37
+ errors: [
38
+ {
39
+ messageId: MESSAGE_IDS.requireVariantsCallStylesName,
40
+ suggestions: [
41
+ {
42
+ messageId: MESSAGE_IDS.renameAllOccurrences,
43
+ output: `
44
+ const buttonVariants = tv({});
45
+ const styles = buttonVariants();
46
+ `,
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ name: "Variant function call not named 'styles' (default)",
52
+ },
53
+ {
54
+ code: `
55
+ const cardVariants = tv({});
56
+ const styles = cardVariants();
57
+ `,
58
+ errors: [
59
+ {
60
+ messageId: MESSAGE_IDS.requireVariantsCallStylesName,
61
+ suggestions: [
62
+ {
63
+ messageId: MESSAGE_IDS.renameAllOccurrences,
64
+ output: `
65
+ const cardVariants = tv({});
66
+ const variants = cardVariants();
67
+ `,
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ name: "Variant function call not named custom 'variants'",
73
+ options: [{ name: "variants" }],
74
+ },
75
+ {
76
+ code: `
77
+ const buttonVariants = tv({});
78
+ const buttonStyles = buttonVariants();
79
+ console.log(buttonStyles);
80
+ const result = buttonStyles();
81
+ `,
82
+ errors: [
83
+ {
84
+ messageId: MESSAGE_IDS.requireVariantsCallStylesName,
85
+ suggestions: [
86
+ {
87
+ messageId: MESSAGE_IDS.renameAllOccurrences,
88
+ output: `
89
+ const buttonVariants = tv({});
90
+ const styles = buttonVariants();
91
+ console.log(styles);
92
+ const result = styles();
93
+ `,
94
+ },
95
+ ],
96
+ },
97
+ ],
98
+ name: "Renames declaration and all references via suggestion",
99
+ },
100
+ ];
101
+
102
+ tester.run("require-variants-call-styles-name", rule, {
103
+ invalid,
104
+ valid,
105
+ });
@@ -0,0 +1,146 @@
1
+ export const MESSAGE_IDS = {
2
+ renameAllOccurrences: "renameAllOccurrences",
3
+ requireVariantsSuffix: "requireVariantsSuffix",
4
+ };
5
+
6
+ /** @typedef {typeof MESSAGE_IDS[keyof typeof MESSAGE_IDS]} MessageIds */
7
+
8
+ /**
9
+ * @typedef {object} RuleOptions
10
+ * @property {string} [suffix="Variants"] Suffix required for variables assigned from tv().
11
+ */
12
+
13
+ /** @type {import("eslint").Rule.RuleModule} */
14
+ export const rule = {
15
+ create: (context) => {
16
+ const [options = {}] = /** @type {[RuleOptions]} */ (context.options);
17
+ const suffix = options.suffix ?? "Variants";
18
+
19
+ return {
20
+ VariableDeclarator(node) {
21
+ detectTvVariableNameViolation(node, context, suffix);
22
+ },
23
+ };
24
+ },
25
+ meta: {
26
+ defaultOptions: [
27
+ {
28
+ suffix: "Variants",
29
+ },
30
+ ],
31
+ docs: {
32
+ description:
33
+ "Require variables assigned from tv() to end with a given suffix.",
34
+ },
35
+ hasSuggestions: true,
36
+ messages: {
37
+ renameAllOccurrences: "Rename all occurrences to {{newName}}",
38
+ requireVariantsSuffix:
39
+ "Variable assigned from tv() must end with '{{suffix}}'.",
40
+ },
41
+ schema: [
42
+ {
43
+ additionalProperties: false,
44
+ properties: {
45
+ suffix: {
46
+ default: "Variants",
47
+ description: "Suffix required for variables assigned from tv().",
48
+ type: "string",
49
+ },
50
+ },
51
+ type: "object",
52
+ },
53
+ ],
54
+ type: "suggestion",
55
+ },
56
+ };
57
+
58
+ /**
59
+ * Check if expression is a tv() call.
60
+ * @param {import("estree").Expression} init
61
+ * @returns {boolean} `true` if expression is a tv() call.
62
+ */
63
+ const isTvCallExpression = (init) =>
64
+ init.type === "CallExpression" &&
65
+ init.callee.type === "Identifier" &&
66
+ init.callee.name === "tv";
67
+
68
+ /**
69
+ * Collect all references to a variable declaration, excluding the initial
70
+ * declaration.
71
+ * @param {object} options
72
+ * @param {import("eslint").SourceCode} options.sourceCode ESLint SourceCode instance.
73
+ * @param {import("estree").VariableDeclarator} options.declaratorNode VariableDeclarator node of the declaration.
74
+ * @param {import("estree").Identifier} options.id Identifier node of the variable being declared.
75
+ * @returns {import("estree").Identifier[]} Array of identifier nodes referencing the variable.
76
+ */
77
+ const collectReferences = (options) => {
78
+ const declaredVariables = options.sourceCode.getDeclaredVariables(
79
+ options.declaratorNode,
80
+ );
81
+
82
+ /** @type {import("estree").Identifier[]} */
83
+ const references = [];
84
+
85
+ for (const variable of declaredVariables) {
86
+ if (variable.name === options.id.name) {
87
+ for (const reference of variable.references) {
88
+ if (
89
+ reference.identifier !== options.id &&
90
+ reference.identifier.type === "Identifier"
91
+ ) {
92
+ references.push(reference.identifier);
93
+ }
94
+ }
95
+ break;
96
+ }
97
+ }
98
+
99
+ return references;
100
+ };
101
+
102
+ /**
103
+ * Detect tv() variable naming violations and report with suggestion to rename all occurrences.
104
+ * @param {import("estree").VariableDeclarator} node
105
+ * @param {import("eslint").Rule.RuleContext} context
106
+ * @param {string} suffix Required variable name suffix.
107
+ */
108
+ const detectTvVariableNameViolation = (node, context, suffix) => {
109
+ const { init, id } = node;
110
+
111
+ if (!init || !isTvCallExpression(init)) {
112
+ return;
113
+ }
114
+
115
+ if (id?.type !== "Identifier" || id.name.endsWith(suffix)) {
116
+ return;
117
+ }
118
+
119
+ const newName = `${id.name}${suffix}`;
120
+ const references = collectReferences({
121
+ declaratorNode: node,
122
+ id,
123
+ sourceCode: context.sourceCode,
124
+ });
125
+
126
+ context.report({
127
+ data: { suffix },
128
+ messageId: MESSAGE_IDS.requireVariantsSuffix,
129
+ node,
130
+ suggest: [
131
+ {
132
+ data: { newName },
133
+ fix: (fixer) => {
134
+ const fixes = [fixer.replaceText(id, newName)];
135
+
136
+ for (const reference of references) {
137
+ fixes.push(fixer.replaceText(reference, newName));
138
+ }
139
+
140
+ return fixes;
141
+ },
142
+ messageId: MESSAGE_IDS.renameAllOccurrences,
143
+ },
144
+ ],
145
+ });
146
+ };
@@ -0,0 +1,81 @@
1
+ import { RuleTester } from "eslint";
2
+
3
+ import { MESSAGE_IDS, rule } from "./require-variants-suffix";
4
+
5
+ const tester = new RuleTester();
6
+
7
+ /** @type {import("eslint").RuleTester.ValidTestCase[]} */
8
+ const valid = [
9
+ {
10
+ code: `const buttonVariants = tv({})`,
11
+ name: "Variable ends with 'Variants' suffix (default)",
12
+ },
13
+ {
14
+ code: `const buttonStyles = tv({})`,
15
+ name: "Variable ends with custom 'Styles' suffix",
16
+ options: [{ suffix: "Styles" }],
17
+ },
18
+ ];
19
+
20
+ /** @type {import("eslint").RuleTester.InvalidTestCase[]} */
21
+ const invalid = [
22
+ {
23
+ code: `const button = tv({})`,
24
+ errors: [
25
+ {
26
+ messageId: MESSAGE_IDS.requireVariantsSuffix,
27
+ suggestions: [
28
+ {
29
+ messageId: MESSAGE_IDS.renameAllOccurrences,
30
+ output: `const buttonVariants = tv({})`,
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ name: "Variable does not end with 'Variants' suffix (default)",
36
+ },
37
+ {
38
+ code: `const button = tv({})`,
39
+ errors: [
40
+ {
41
+ messageId: MESSAGE_IDS.requireVariantsSuffix,
42
+ suggestions: [
43
+ {
44
+ messageId: MESSAGE_IDS.renameAllOccurrences,
45
+ output: `const buttonStyles = tv({})`,
46
+ },
47
+ ],
48
+ },
49
+ ],
50
+ name: "Variable does not end with custom 'Styles' suffix",
51
+ options: [{ suffix: "Styles" }],
52
+ },
53
+ {
54
+ code: `
55
+ const button = tv({});
56
+ console.log(button);
57
+ const result = button;
58
+ `,
59
+ errors: [
60
+ {
61
+ messageId: MESSAGE_IDS.requireVariantsSuffix,
62
+ suggestions: [
63
+ {
64
+ messageId: MESSAGE_IDS.renameAllOccurrences,
65
+ output: `
66
+ const buttonVariants = tv({});
67
+ console.log(buttonVariants);
68
+ const result = buttonVariants;
69
+ `,
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ name: "Renames declaration and all references via suggestion",
75
+ },
76
+ ];
77
+
78
+ tester.run("require-variants-suffix", rule, {
79
+ invalid,
80
+ valid,
81
+ });