Skip to content

Commit 69dcd7b

Browse files
refactor: joiner ❤️
1 parent f274f0d commit 69dcd7b

5 files changed

Lines changed: 103 additions & 94 deletions

File tree

src/rules/classnames-order.ts

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { RuleCreator } from "@typescript-eslint/utils/eslint-utils";
88
import { RuleContext as TSESLintRuleContext } from "@typescript-eslint/utils/ts-eslint";
99

1010
import urlCreator from "../url-creator";
11+
import { joiner } from "../utils/joiner";
1112
import {
1213
parsePluginSettings,
1314
PluginSettings,
@@ -42,30 +43,6 @@ type RuleContext = TSESLintRuleContext<MessageIds, Options>;
4243
// The parameter passed into RuleCreator is a URL generator function.
4344
export const createRule = RuleCreator(urlCreator);
4445

45-
const joinSortedClassnames = (
46-
classNames: Array<string>,
47-
whitespaces: Array<string>,
48-
headSpace: boolean,
49-
tailSpace: boolean,
50-
) => {
51-
// Make a copy of whitespaces because we don't want to mutate the original array
52-
// (Remember that ESLint runs several times and we don't want to mess up the whitespaces for the next runs)
53-
const spaces = [...whitespaces];
54-
55-
const head = headSpace ? spaces.shift() : "";
56-
const tail = tailSpace ? spaces.pop() : "";
57-
58-
const validatedClasses: Array<string> = [];
59-
for (const [index, className] of classNames.entries()) {
60-
const spacer =
61-
validatedClasses.length === 0 ? "" : (spaces[index - 1] ?? " ");
62-
validatedClasses.push(spacer + className);
63-
}
64-
65-
if (validatedClasses.length === 0) return "";
66-
return head + validatedClasses.join("") + tail;
67-
};
68-
6946
const sortClassnames = (
7047
context: RuleContext,
7148
settings: PluginSettings,
@@ -87,12 +64,12 @@ const sortClassnames = (
8764
);
8865

8966
// Generates the validated/sorted attribute value
90-
let validatedClassNamesValue = joinSortedClassnames(
91-
orderedClassNames,
67+
let validatedClassNamesValue = joiner({
68+
classNames: orderedClassNames,
9269
whitespaces,
9370
headSpace,
9471
tailSpace,
95-
);
72+
});
9673

9774
if (originalClassNamesValue !== validatedClassNamesValue) {
9875
validatedClassNamesValue = prefix + validatedClassNamesValue + suffix;

src/rules/no-contradicting-classname.ts

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { RuleContext as TSESLintRuleContext } from "@typescript-eslint/utils/ts-
99

1010
import urlCreator from "../url-creator";
1111
import { getPropertiesFromCssRule } from "../utils/get-properties-from-css-rule";
12+
import { joiner } from "../utils/joiner";
1213
import { mapGetKeyFromSetValues } from "../utils/map";
1314
import {
1415
parsePluginSettings,
@@ -83,32 +84,6 @@ const getCommonProperties = (groupMembers: Map<string, Set<string>>) => {
8384
return commonProperties;
8485
};
8586

86-
const removeContradictions = (
87-
targets: Array<string>,
88-
classNames: Array<string>,
89-
whitespaces: Array<string>,
90-
headSpace: boolean,
91-
tailSpace: boolean,
92-
) => {
93-
// Make a copy of whitespaces because we don't want to mutate the original array
94-
// (Remember that ESLint runs several times and we don't want to mess up the whitespaces for the next runs)
95-
const spaces = [...whitespaces];
96-
97-
const head = headSpace ? spaces.shift() : "";
98-
const tail = tailSpace ? spaces.pop() : "";
99-
100-
const validatedClasses: Array<string> = [];
101-
for (const [index, className] of classNames.entries()) {
102-
if (!targets.includes(className)) {
103-
const spacer =
104-
validatedClasses.length === 0 ? "" : (spaces[index - 1] ?? " ");
105-
validatedClasses.push(spacer + className);
106-
}
107-
}
108-
if (validatedClasses.length === 0) return "";
109-
return head + validatedClasses.join("") + tail;
110-
};
111-
11287
const getContradictions = (
11388
context: RuleContext,
11489
settings: PluginSettings,
@@ -164,13 +139,13 @@ const getContradictions = (
164139
const otherClassnamesFormatted = otherClassnames
165140
.map((cn) => `'${cn}'`)
166141
.join(", ");
167-
let patchedValue = removeContradictions(
168-
otherClassnames,
142+
let patchedValue = joiner({
169143
classNames,
170144
whitespaces,
171145
headSpace,
172146
tailSpace,
173-
);
147+
validator: (cls) => !otherClassnames.includes(cls),
148+
});
174149
patchedValue = prefix + patchedValue + suffix;
175150
const patchedLoc = generateLocForClassname(
176151
node,
@@ -221,7 +196,6 @@ export const noContradictingClassname = createRule<Options, MessageIds>({
221196
"issue:contradiction": `'{{classname}}' conflicts with {{otherClassnames}}.`,
222197
"fix:contradiction:keep": `Keep '{{keepClassname}}' (remove {{removeClassnames}}).`,
223198
},
224-
// fixable: "code",
225199
// Schema is also parsed by `eslint-doc-generator`
226200
schema: [
227201
{

src/rules/no-custom-classname.ts

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { RuleCreator } from "@typescript-eslint/utils/eslint-utils";
77
import { RuleContext as TSESLintRuleContext } from "@typescript-eslint/utils/ts-eslint";
88

99
import urlCreator from "../url-creator";
10+
import { joiner } from "../utils/joiner";
1011
import {
1112
parsePluginSettings,
1213
PluginSettings,
@@ -52,33 +53,6 @@ const isWhitelisted = (className: string, whitelist: Set<string>): boolean => {
5253
return [...whitelist].some((pattern) => passRegexTest(pattern, className));
5354
};
5455

55-
const removeClassname = (
56-
invalidClassName: string,
57-
classNames: Array<string>,
58-
whitespaces: Array<string>,
59-
headSpace: boolean,
60-
tailSpace: boolean,
61-
) => {
62-
// Make a copy of whitespaces because we don't want to mutate the original array
63-
// (Remember that ESLint runs several times and we don't want to mess up the whitespaces for the next runs)
64-
const spaces = [...whitespaces];
65-
66-
const head = headSpace ? spaces.shift() : "";
67-
const tail = tailSpace ? spaces.pop() : "";
68-
69-
const validatedClasses: Array<string> = [];
70-
for (const [index, className] of classNames.entries()) {
71-
if (className !== invalidClassName) {
72-
const spacer =
73-
validatedClasses.length === 0 ? "" : (spaces[index - 1] ?? " ");
74-
validatedClasses.push(spacer + className);
75-
}
76-
}
77-
78-
if (validatedClasses.length === 0) return "";
79-
return head + validatedClasses.join("") + tail;
80-
};
81-
8256
const detectCustomClassnames = (
8357
context: RuleContext,
8458
settings: PluginSettings,
@@ -99,25 +73,25 @@ const detectCustomClassnames = (
9973
// Process the extracted classnames and report
10074
const { classNames, whitespaces, headSpace, tailSpace } =
10175
getClassnamesFromValue(originalClassNamesValue);
102-
for (const cls of classNames) {
103-
if (isWhitelisted(cls, mergedWhitelist)) continue;
104-
if (isValidClassNameWorker(settings.cssConfigPath, cls)) continue;
76+
for (const customClass of classNames) {
77+
if (isWhitelisted(customClass, mergedWhitelist)) continue;
78+
if (isValidClassNameWorker(settings.cssConfigPath, customClass)) continue;
10579

10680
// Generates the "cleaned" attribute value
107-
let patchedValue = removeClassname(
108-
cls,
81+
let patchedValue = joiner({
10982
classNames,
11083
whitespaces,
11184
headSpace,
11285
tailSpace,
113-
);
86+
validator: (candidate) => candidate !== customClass,
87+
});
11488
const patchedLoc = generateLocForClassname(
11589
node,
116-
cls,
90+
customClass,
11791
originalClassNamesValue,
11892
genericContext,
11993
);
120-
const range = getRange(node, cls, originalClassNamesValue);
94+
const range = getRange(node, customClass, originalClassNamesValue);
12195
patchedValue = prefix + patchedValue + suffix;
12296

12397
if (originalClassNamesValue === patchedValue) {
@@ -127,15 +101,15 @@ const detectCustomClassnames = (
127101
loc: patchedLoc,
128102
messageId: "issue:unknown-classname",
129103
data: {
130-
classname: cls,
104+
classname: customClass,
131105
},
132106
suggest:
133107
range[0] && range[1]
134108
? [
135109
{
136110
messageId: "fix:unknown-classname:remove",
137111
data: {
138-
classname: cls,
112+
classname: customClass,
139113
},
140114
fix: (fixer) =>
141115
fixer.replaceTextRange([start, end], patchedValue),

src/utils/joiner.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect, test } from "vitest";
2+
3+
import { joiner } from "./joiner";
4+
5+
test(`joiner() with head and tail spaces`, () => {
6+
const classNames = ["a", "x", "b"];
7+
const whitespaces = ["\n ", " ", " ", "\n "];
8+
expect(
9+
joiner({
10+
classNames,
11+
whitespaces,
12+
headSpace: true,
13+
tailSpace: true,
14+
}),
15+
).toBe("\n a x b\n ");
16+
17+
expect(
18+
joiner({
19+
classNames,
20+
whitespaces,
21+
headSpace: true,
22+
tailSpace: true,
23+
validator: (cls) => cls !== "x",
24+
}),
25+
).toBe("\n a b\n ");
26+
});
27+
28+
test(`joiner() without head nor tail spaces`, () => {
29+
const classNames = ["a", "x", "b"];
30+
const whitespaces = [" ", " "];
31+
expect(
32+
joiner({
33+
classNames,
34+
whitespaces,
35+
headSpace: false,
36+
tailSpace: false,
37+
}),
38+
).toBe("a x b");
39+
40+
expect(
41+
joiner({
42+
classNames,
43+
whitespaces,
44+
headSpace: false,
45+
tailSpace: false,
46+
validator: (cls) => cls !== "x",
47+
}),
48+
).toBe("a b");
49+
});

src/utils/joiner.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
type JoinerOptions = {
2+
classNames: Array<string>;
3+
whitespaces: Array<string>;
4+
headSpace: boolean;
5+
tailSpace: boolean;
6+
validator?: (candidate: string) => boolean;
7+
};
8+
/**
9+
* Helper function to join classnames with whitespaces, and preserve head/tail spaces if needed.
10+
*/
11+
export const joiner = ({
12+
classNames,
13+
whitespaces,
14+
headSpace,
15+
tailSpace,
16+
validator = () => true,
17+
}: JoinerOptions) => {
18+
// Make a copy of whitespaces because we don't want to mutate the original array
19+
// (Remember that ESLint runs several times and we don't want to mess up the whitespaces for the next runs)
20+
const spaces = [...whitespaces];
21+
22+
const head = headSpace ? spaces.shift() : "";
23+
const tail = tailSpace ? spaces.pop() : "";
24+
25+
const validatedClasses: Array<string> = [];
26+
for (const [index, className] of classNames.entries()) {
27+
if (!validator(className)) continue;
28+
const spacer =
29+
validatedClasses.length === 0 ? "" : (spaces[index - 1] ?? " ");
30+
validatedClasses.push(spacer + className);
31+
}
32+
33+
if (validatedClasses.length === 0) return "";
34+
return head + validatedClasses.join("") + tail;
35+
};

0 commit comments

Comments
 (0)