1- /* eslint-disable perfectionist/sort-objects */
21import * as NodeContext from "@effect/platform-node/NodeContext" ;
32import * as NodeRuntime from "@effect/platform-node/NodeRuntime" ;
43import * 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)
2727const 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
3637const 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
4345const 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 = / ^ p a c k a g e s \/ p l u g i n s \/ e s l i n t - p l u g i n - r e a c t - ( [ ^ / ] + ) / u. exec ( file ) ?. [ 1 ] ?? "" ;
97+ // Extract category and rule name from file path
98+ const category = / ^ p a c k a g e s \/ p l u g i n s \/ e s l i n t - p l u g i n - r e a c t - ( [ ^ / ] + ) / 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 ( / \. t s $ / 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 ( / \. t s $ / 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
193237const 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
200244program . pipe ( Effect . provide ( NodeContext . layer ) , NodeRuntime . runMain ) ;
0 commit comments