Skip to content

Commit 4a6fab9

Browse files
committed
Minor scripts improvements
1 parent 381bbe6 commit 4a6fab9

File tree

1 file changed

+96
-52
lines changed

1 file changed

+96
-52
lines changed

scripts/verify-rules-metas.ts

Lines changed: 96 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable perfectionist/sort-objects */
21
import * as NodeContext from "@effect/platform-node/NodeContext";
32
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
43
import * as FileSystem from "@effect/platform/FileSystem";
@@ -24,22 +23,25 @@ const SECTION_HEADERS = [
2423
// { key: "debug", heading: "Debug Rules" },
2524
];
2625

26+
// Convert ESLint severity config to numeric value (0=off, 1=warn, 2=error)
2727
const getSeverity = (x: unknown): number =>
2828
match(x)
2929
.with("off", () => 0)
3030
.with("warn", () => 1)
3131
.with("error", () => 2)
3232
.with(P.number, (n) => n)
33-
.with(P.array(), ([s]) => getSeverity(s))
33+
.with(P.array(), ([s]) => getSeverity(s)) // Handle array config like ["error", options]
3434
.otherwise(() => 0);
3535

36+
// Map severity level to emoji indicator
3637
const getSeverityIcon = (x: unknown) =>
3738
match(x)
3839
.with(0, () => "0️⃣")
3940
.with(1, () => "1️⃣")
4041
.with(2, () => "2️⃣")
4142
.otherwise(() => "0️⃣");
4243

44+
// Map rule feature flags to emoji indicators
4345
const getFeatureIcon = (x: unknown) =>
4446
match(x)
4547
.with("CFG", () => "⚙️")
@@ -50,135 +52,177 @@ const getFeatureIcon = (x: unknown) =>
5052
.with("EXP", () => "🧪")
5153
.otherwise(() => "");
5254

53-
function retrieveRuleMeta(catename: string, rulename: string) {
55+
// Extract metadata from a rule module (name, description, features, severities)
56+
function retrieveRuleMeta(category: string, name: string) {
5457
return Effect.gen(function*() {
55-
const filename = `packages/plugins/eslint-plugin-react-${catename}/src/rules/${rulename}.ts`;
56-
const { default: ruleModule, RULE_FEATURES, RULE_NAME } = yield* Effect.tryPromise(() => import(`../${filename}`));
57-
const description = match(ruleModule)
58+
const filename = `packages/plugins/eslint-plugin-react-${category}/src/rules/${name}.ts`;
59+
const { default: mod, RULE_FEATURES, RULE_NAME } = yield* Effect.tryPromise(() => import(`../${filename}`));
60+
61+
// Extract description from rule's meta. docs
62+
const description = match(mod)
5863
.with({ meta: { docs: { description: P.select(P.string) } } }, identity)
5964
.otherwise(() => "No description available.");
65+
66+
// Look up severity in recommended and strict presets
6067
const rEntry = Reflect.get(
6168
config0.rules,
62-
catename === "x"
69+
category === "x"
6370
? `@eslint-react/${RULE_NAME}`
64-
: `@eslint-react/${catename}/${RULE_NAME}`,
71+
: `@eslint-react/${category}/${RULE_NAME}`,
6572
);
6673
const sEntry = Reflect.get(
6774
config1.rules,
68-
catename === "x"
75+
category === "x"
6976
? `@eslint-react/${RULE_NAME}`
70-
: `@eslint-react/${catename}/${RULE_NAME}`,
77+
: `@eslint-react/${category}/${RULE_NAME}`,
7178
);
79+
7280
return {
7381
name: RULE_NAME,
82+
// eslint-disable-next-line perfectionist/sort-objects
7483
description,
7584
features: RULE_FEATURES,
76-
severities: [
77-
getSeverity(rEntry),
78-
getSeverity(sEntry),
79-
],
85+
severities: [getSeverity(rEntry), getSeverity(sEntry)], // [recommended, strict]
8086
};
8187
});
8288
}
8389

84-
const verifyRulesMarkdowns = Effect.gen(function*() {
90+
// Verify each rule's .mdx documentation matches its source metadata
91+
const verifyDocs = Effect.gen(function*() {
8592
const fs = yield* FileSystem.FileSystem;
8693
const path = yield* Path.Path;
8794
const files = glob(RULES_GLOB).filter((file) => !file.endsWith(".spec.ts"));
95+
8896
for (const file of files) {
89-
const catename = /^packages\/plugins\/eslint-plugin-react-([^/]+)/u.exec(file)?.[1] ?? "";
97+
// Extract category and rule name from file path
98+
const category = /^packages\/plugins\/eslint-plugin-react-([^/]+)/u.exec(file)?.[1] ?? "";
9099
const basename = path.parse(path.basename(file)).name;
91100
const filename = path.resolve(file);
92-
const rulename = `${catename}/${basename}`;
93-
const meta = yield* retrieveRuleMeta(catename, basename);
94-
const docContent = yield* fs.readFileString(filename.replace(/\.ts$/u, ".mdx"), "utf8");
95-
const docContentLines = docContent.split("\n");
96-
const expectedDescription = meta.description;
97-
const descriptionLineIndex = docContentLines.findIndex((line) => line.startsWith("## Description"));
98-
if (descriptionLineIndex === -1) {
101+
const rulename = `${category}/${basename}`;
102+
const rulemeta = yield* retrieveRuleMeta(category, basename);
103+
104+
// Read corresponding . mdx documentation file
105+
const content = yield* fs.readFileString(filename.replace(/\.ts$/u, ".mdx"), "utf8");
106+
const contentLines = content.split("\n");
107+
108+
// Verify description section matches rule metadata
109+
const expectedDescription = rulemeta.description;
110+
const descriptionIndex = contentLines.findIndex((line) => line.startsWith("## Description"));
111+
if (descriptionIndex === -1) {
99112
yield* Effect.logError(ansis.red(` Missing description line in documentation for rule ${rulename}`));
100113
continue;
101114
}
102-
const providedDescription = docContentLines[descriptionLineIndex + 2]?.trim().replaceAll("`", "'");
115+
const providedDescription = contentLines[descriptionIndex + 2]?.trim().replaceAll("`", "'");
103116
if (providedDescription == null || !providedDescription.includes(expectedDescription.replace(/\.$/, ""))) {
104117
yield* Effect.logError(ansis.red(` Found 1 mismatched description in documentation for rule ${rulename}`));
105118
yield* Effect.logError(` Expected: ${ansis.bgGreen(expectedDescription)}`);
106119
yield* Effect.logError(` Provided: ${ansis.bgYellow(providedDescription)}`);
107120
}
108-
const featuresLineIndex = docContentLines.findIndex((line) => line.startsWith("**Features**"));
109-
if (featuresLineIndex === -1) {
110-
if (meta.features.length === 0) continue;
121+
122+
// Verify features section matches rule metadata
123+
const featuresIndex = contentLines.findIndex((line) => line.startsWith("**Features**"));
124+
if (featuresIndex === -1) {
125+
if (rulemeta.features.length === 0) continue;
111126
yield* Effect.logError(ansis.red(` Missing features line in documentation for rule ${rulename}`));
112127
continue;
113128
}
114-
const expectedFeatureIcons = meta.features.map(getFeatureIcon).map((icon: string) => "`" + icon + "`").join(" ");
115-
const providedFeatureIcons = docContentLines[featuresLineIndex + 2]?.trim() ?? "";
129+
const expectedFeatureIcons = rulemeta
130+
.features
131+
.map(getFeatureIcon)
132+
.map((icon: string) => "`" + icon + "`")
133+
.join(" ");
134+
const providedFeatureIcons = contentLines[featuresIndex + 2]?.trim() ?? "";
116135
if (expectedFeatureIcons !== providedFeatureIcons) {
117136
yield* Effect.logError(ansis.red(` Found 1 mismatched feature icons in documentation for rule ${rulename}`));
118137
yield* Effect.logError(` Expected: ${ansis.bgGreen(expectedFeatureIcons)}`);
119138
yield* Effect.logError(` Provided: ${ansis.bgYellow(providedFeatureIcons)}`);
120139
}
121-
// TODO: Verify presets section as well
140+
141+
// Verify presets section exists if rule has non-zero severities
142+
const presetsIndex = contentLines.findIndex((line) => line.startsWith("**Presets**"));
143+
if (presetsIndex === -1) {
144+
if (rulemeta.severities.every((s) => s === 0)) continue;
145+
yield* Effect.logError(ansis.red(` Missing presets line in documentation for rule ${rulename}`));
146+
continue;
147+
}
148+
// TODO: Verify presets content if needed
122149
}
123150
});
124151

125-
const verifyRulesOverview = Effect.gen(function*() {
152+
// Verify the overview. mdx table entries match actual rule metadata
153+
const verifyOverview = Effect.gen(function*() {
126154
const fs = yield* FileSystem.FileSystem;
127155
const path = yield* Path.Path;
128-
const targetPath = path.join(...RULES_OVERVIEW_PATH);
129-
const content = yield* fs.readFileString(targetPath, "utf8");
156+
const target = path.join(...RULES_OVERVIEW_PATH);
157+
const content = yield* fs.readFileString(target, "utf8");
130158
const contentLines = content.split("\n");
131-
yield* Effect.log(ansis.green(`Verifying rules overview at ${targetPath}...`));
159+
160+
yield* Effect.log(ansis.green(`Verifying rules overview at ${target}...`));
161+
162+
// Process each rule category section
132163
for (const { key, heading } of SECTION_HEADERS) {
164+
// Locate section heading and table boundaries
133165
const headerStartIndex = contentLines.findIndex((line) => line.startsWith(`## ${heading}`));
134166
if (headerStartIndex === -1) {
135-
return yield* Effect.dieMessage(`Could not find section for ${heading} in ${targetPath}`);
167+
return yield* Effect.dieMessage(`Could not find section for ${heading} in ${target}`);
136168
}
137169
const tableStartIndex = contentLines
138170
.findIndex((line, index) => index > headerStartIndex && line.startsWith("| Rule"));
139171
if (tableStartIndex === -1) {
140-
return yield* Effect.dieMessage(`Could not find table for ${heading} in ${targetPath}`);
172+
return yield* Effect.dieMessage(`Could not find table for ${heading} in ${target}`);
141173
}
142-
const tableEndIndex = contentLines
143-
.findIndex((line, index) => index > tableStartIndex && line.trim() === "");
144-
if (tableEndIndex === -1) {
145-
return yield* Effect.dieMessage(`Could not find the end of table for ${heading} in ${targetPath}`);
174+
const endIndex = contentLines.findIndex((line, index) => index > tableStartIndex && line.trim() === "");
175+
if (endIndex === -1) {
176+
return yield* Effect.dieMessage(`Could not find the end of table for ${heading} in ${target}`);
146177
}
147-
const tableLines = contentLines.slice(tableStartIndex + 2, tableEndIndex);
178+
179+
// Verify each table row (skip header and separator rows)
180+
const tableLines = contentLines.slice(tableStartIndex + 2, endIndex);
148181
for (const line of tableLines) {
149-
const columns = line.split("|").slice(1, -1);
182+
const columns = line.split("|").slice(1, -1); // Remove leading/trailing empty splits
150183
const [link, severities, features, description] = columns;
151184
if (link == null || severities == null || features == null || description == null) {
152185
yield* Effect.logError(ansis.red(`Malformed table line (skipped): ${line}`));
153186
continue;
154187
}
155-
const catename = key;
188+
189+
const category = key;
156190
const rulename = link.match(/\[`([^`]+)`\]/)?.[1];
157191
if (rulename == null) {
158-
return yield* Effect.dieMessage(`Could not extract rule name from link: ${link}`);
192+
yield* Effect.logError(ansis.red(`Could not extract rule name from link (skipped): ${link}`));
193+
continue;
159194
}
160-
const meta = yield* retrieveRuleMeta(catename, rulename);
161-
const expectedRuleLink = `[\`${rulename}\`](${catename === "x" ? "" : catename + "-"}${rulename})`;
162-
const providedRuleLink = link.trim();
163-
if (expectedRuleLink !== providedRuleLink) {
195+
196+
const meta = yield* retrieveRuleMeta(category, rulename);
197+
198+
// Verify link format
199+
const expectedLink = `[\`${rulename}\`](${category === "x" ? "" : category + "-"}${rulename})`;
200+
const providedLink = link.trim();
201+
if (expectedLink !== providedLink) {
164202
yield* Effect.logError(ansis.red(`Found 1 mismatched link for rule ${rulename}`));
165-
yield* Effect.logError(` Expected: ${ansis.bgGreen(expectedRuleLink)}`);
166-
yield* Effect.logError(` Provided: ${ansis.bgYellow(providedRuleLink)}`);
203+
yield* Effect.logError(` Expected: ${ansis.bgGreen(expectedLink)}`);
204+
yield* Effect.logError(` Provided: ${ansis.bgYellow(providedLink)}`);
167205
}
206+
207+
// Verify description text
168208
const expectedDescription = meta.description.replace(/\.$/, "");
169209
const providedDescription = description.trim().replaceAll("`", "'");
170210
if (expectedDescription !== providedDescription) {
171211
yield* Effect.logError(ansis.red(`Found 1 mismatched description for rule ${rulename}`));
172212
yield* Effect.logError(` Expected: ${ansis.bgGreen(expectedDescription)}`);
173213
yield* Effect.logError(` Provided: ${ansis.bgYellow(providedDescription)}`);
174214
}
215+
216+
// Verify severity icons match preset configurations
175217
const expectedSeverityIcons = `${getSeverityIcon(meta.severities[0])} ${getSeverityIcon(meta.severities[1])}`;
176218
const providedSeverityIcons = severities.trim();
177219
if (expectedSeverityIcons !== providedSeverityIcons) {
178220
yield* Effect.logError(ansis.red(`Found 1 mismatched severity icons for rule ${rulename}`));
179221
yield* Effect.logError(` Expected: ${ansis.bgGreen(expectedSeverityIcons)}`);
180222
yield* Effect.logError(` Provided: ${ansis.bgYellow(providedSeverityIcons)}`);
181223
}
224+
225+
// Verify feature icons match rule features
182226
const expectedFeatureIcons = meta.features.map(getFeatureIcon).map((icon: string) => "`" + icon + "`").join(" ");
183227
const providedFeatureIcons = features.trim();
184228
if (expectedFeatureIcons !== providedFeatureIcons) {
@@ -192,9 +236,9 @@ const verifyRulesOverview = Effect.gen(function*() {
192236

193237
const program = Effect.gen(function*() {
194238
// Verify the rules overview matches the actual rule definitions
195-
yield* verifyRulesOverview;
239+
yield* verifyOverview;
196240
// Verify the rules documentations match the actual rule definitions
197-
yield* verifyRulesMarkdowns;
241+
yield* verifyDocs;
198242
});
199243

200244
program.pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);

0 commit comments

Comments
 (0)