Skip to content
This repository was archived by the owner on Dec 5, 2024. It is now read-only.

Commit ee94cdf

Browse files
feat: add lint fixes and suggestions
1 parent 0e9b87f commit ee94cdf

File tree

9 files changed

+104
-32
lines changed

9 files changed

+104
-32
lines changed

example/.eslintrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["plugin:@jespers/css-modules/recommended"]
3+
}

example/foo.module.css

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.bar {
2+
color: red;
3+
}
4+
5+
.baz {
6+
color: green;
7+
}
8+
9+
.biz {
10+
color: blue;
11+
}
12+
13+
.block {
14+
display: block;
15+
}
16+
17+
.flex {
18+
display: flex;
19+
}

example/foo.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint @jespers/css-modules/no-unused-classes: [2, { markAsUsed: ['baz', 'biz', 'block', 'flex'] }] */
2+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
3+
// @ts-ignore
4+
import styles from "./foo.module.css";
5+
6+
console.log(styles.bar);

example/tsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"noEmit": true,
4+
"paths": {
5+
"@jesper/eslint-plugin-css-modules": ["../"]
6+
}
7+
}
8+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"scripts": {
3030
"build": "tsc",
3131
"test": "jest",
32+
"postinstall": "yarn link && yarn link \"@jespers/eslint-plugin-css-modules\"",
3233
"release": "tsc && release-it"
3334
},
3435
"dependencies": {

src/configs/recommended/recommended.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
const recommended = {
1+
import { TSESLint } from "@typescript-eslint/utils";
2+
3+
const recommended: TSESLint.CLIEngine.Options = {
24
parser: "@typescript-eslint/parser",
35
parserOptions: {
46
sourceType: "module",
7+
extraFileExtensions: [".css"],
58
},
9+
plugins: ["@jespers/css-modules"],
610
rules: {
7-
"css-modules/no-unused-classes": "error",
11+
"@jespers/css-modules/no-unused-classes": "error",
812
},
913
};
1014

src/rules/no-unused-classes/no-unused-classes.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -44,35 +44,35 @@ ruleTester.run("no-unused-classes", noUnusedClasses, {
4444
invalid: [
4545
{
4646
code: 'import styles from "./component01.module.css";',
47-
errors: [{ messageId: "unusedCssClass" }],
47+
errors: [{ messageId: "unusedCssClasses" }],
4848
},
4949
{
5050
code: `
5151
import styles from "./component01.module.css";
5252
5353
const unused = styles;
5454
`,
55-
errors: [{ messageId: "unusedCssClass" }],
55+
errors: [{ messageId: "unusedCssClasses" }],
5656
},
5757
{
5858
code: 'import styles from "./folder/component02.module.css";',
59-
errors: [{ messageId: "unusedCssClass" }],
59+
errors: [{ messageId: "unusedCssClasses" }],
6060
},
6161
{
6262
code: `
6363
import styles from "./folder/component02.module.css";
6464
6565
const unused = styles;
6666
`,
67-
errors: [{ messageId: "unusedCssClass" }],
67+
errors: [{ messageId: "unusedCssClasses" }],
6868
},
6969
{
7070
code: `
7171
import styles from "./component03.module.css";
7272
7373
const used = styles['main'];
7474
`,
75-
errors: [{ messageId: "unusedCssClass" }],
75+
errors: [{ messageId: "unusedCssClasses" }],
7676
},
7777
],
7878
});

src/rules/no-unused-classes/no-unused-classes.ts

+55-24
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,23 @@ function getMarkAsUsedOption(options: unknown): Set<string> {
9595
return markAsUsedClassNames;
9696
}
9797

98+
function getPrettyUnusedClassNames(unusedClassNames: string[]): string {
99+
const base = unusedClassNames
100+
.slice(0, 3)
101+
.map((className) => `'${className}'`)
102+
.join(", ");
103+
104+
if (unusedClassNames.length <= 3) {
105+
return base;
106+
}
107+
108+
return `${base}... (+${Math.max(0, unusedClassNames.length - 3)} more)`;
109+
}
110+
111+
function getFixPayloadClassNames(unusedClassNames: string[]): string {
112+
return unusedClassNames.map((className) => `'${className}'`).join(", ");
113+
}
114+
98115
const rule = createRule({
99116
name: "no-unused-classes",
100117
defaultOptions: [
@@ -117,28 +134,28 @@ const rule = createRule({
117134
docs: {
118135
description: "Check for any unused classes in imported CSS modules",
119136
recommended: "error",
137+
suggestion: true,
120138
},
121139
messages: {
122-
unusedCssClass:
123-
"Unused CSS class '{{ className }}' in '{{ cssFilePath }}'",
140+
unusedCssClasses:
141+
"Unused CSS classes {{ classNames }} in '{{ cssFilePath }}'",
142+
markAsUsed: "Mark {{ classNames }} as used",
124143
},
144+
hasSuggestions: true,
145+
fixable: "code",
125146
},
126147
create(context) {
127148
type SourceFilePath = string;
128-
type ClassName = string;
129149

130150
const files: Map<
131151
SourceFilePath,
132152
{
133153
availableClassNames: Set<string>;
134154
usedClassNames: Set<string>;
135-
meta?: Map<
136-
ClassName,
137-
{
138-
importNode: TSESTree.ImportDeclaration;
139-
cssFilePath: string;
140-
}
141-
>;
155+
meta?: {
156+
importNode: TSESTree.ImportDeclaration;
157+
cssFilePath: string;
158+
};
142159
}
143160
> = new Map();
144161

@@ -166,7 +183,11 @@ const rule = createRule({
166183
if (classNames.size > 0) {
167184
const file = files.get(sourceFilePath);
168185

169-
const meta = file?.meta ?? new Map();
186+
const meta = file?.meta ?? {
187+
importNode: node,
188+
cssFilePath,
189+
};
190+
170191
const availableClassNames =
171192
file?.availableClassNames ?? new Set();
172193

@@ -182,12 +203,6 @@ const rule = createRule({
182203
classNames.forEach((className) => {
183204
// Add the class to available classes
184205
availableClassNames.add(className);
185-
186-
// Save the AST node and CSS file path for ESLint error use
187-
meta.set(className, {
188-
importNode: node,
189-
cssFilePath,
190-
});
191206
});
192207
}
193208
}
@@ -238,22 +253,38 @@ const rule = createRule({
238253
!markAsUsedClassNames.has(className)
239254
);
240255

241-
// Iterate over the unused class names and create an error report for each of them
242-
unusedClassNames.forEach((className) => {
243-
const { importNode, cssFilePath } = meta?.get(className) ?? {};
256+
if (unusedClassNames.length > 0) {
257+
const { importNode, cssFilePath } = meta ?? {};
258+
259+
if (importNode && cssFilePath) {
260+
const prettyClassNames: string =
261+
getPrettyUnusedClassNames(unusedClassNames);
262+
const fixPayloadClassNames: string =
263+
getFixPayloadClassNames(unusedClassNames);
244264

245-
if (importNode) {
246265
context.report({
247266
node: importNode, // The AST node
248-
messageId: "unusedCssClass", // The error message ID
267+
messageId: "unusedCssClasses", // The error message ID
249268
// Used for string interpolation in the error message
250269
data: {
251-
className, // The unused CSS class name
270+
classNames: prettyClassNames, // The unused CSS class name
252271
cssFilePath, // The CSS file path
253272
},
273+
suggest: [
274+
{
275+
fix: (val) => {
276+
return val.insertTextBeforeRange(
277+
importNode.range,
278+
`/* eslint @jespers/css-modules/no-unused-classes: [2, { markAsUsed: [${fixPayloadClassNames}] }] */\n`
279+
);
280+
},
281+
messageId: "markAsUsed",
282+
data: { classNames: fixPayloadClassNames },
283+
},
284+
],
254285
});
255286
}
256-
});
287+
}
257288
});
258289
},
259290
};

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414
"skipDefaultLibCheck": true,
1515
"moduleResolution": "nodenext"
1616
},
17-
"exclude": ["dist", "src/fixtures"]
17+
"exclude": ["dist", "src/fixtures", "example"]
1818
}

0 commit comments

Comments
 (0)