Skip to content

Commit

Permalink
chore: add doc-renderer for generating & updating detailed rule docs (#…
Browse files Browse the repository at this point in the history
…442)

### Problems:

- The new rule document (generated by `npm run new`) was unclear about which part is the header and which part is the body.
       
- In `tools/update-docs.ts`, when generating notes and adding them to the header section:
  - The process is too much like cutting and pasting manually on the string, it reduces the readability
  - The code for producing the new header (with notes included) is quite fragmented and long
  - Includes legacy/redundant code `header.replace(/\$/g, "$$$$")`
  - Inefficient code, it creates two new items for the notes then `join()` it with `'\n'` later to create the trailing `'\n\n'`
  
   ```ts
       if (notes.length >= 1) {
         notes.push("", "")
       }
       ...
       const header = `\n${title}\n\n${notes.join("\n")}`

    ```

### This PR:
  - Create renderRuleHeader function and a RuleDocHeader type to clarify the structure of the header. Then applies this to both the generation and update tools for the detailed rule Markdown files

```ts
type RuleDocHeader = {
  ruleId: string
  description: string
  notes: string[]
}
const header = renderRuleHeader({ ruleId, description, notes })
```
 - Make it clear there are `\n\n` between each part of the header in the `renderRuleHeader` function
  • Loading branch information
Foxeye-Rinx authored Nov 1, 2024
1 parent ae7c6a4 commit db90a41
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 83 deletions.
97 changes: 97 additions & 0 deletions tools/lib/doc-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { RuleModule } from "src/types"

type RuleDocHeader = {
ruleId: string
description: string
notes: string[]
}
/**
* Render the header of the doc file of a rule.
*
* example header:
* ```
* # astro/no-unused-vars
*
* > description
*
* - note1
* - note2
*
* ```
* Note that there are newlines between the parts of the header
* and there is a trailing newline at the end.
*/
export function renderRuleHeader({
ruleId,
description,
notes,
}: RuleDocHeader): string {
const hasNotes = notes.length > 0
const notesStr = hasNotes ? `${notes.join("\n")}\n\n` : ""
return `# ${ruleId}\n\n> ${description}\n\n${notesStr}`
}

//eslint-disable-next-line jsdoc/require-jsdoc -- tools
function formatItems(items: string[]) {
if (items.length <= 2) {
return items.join(" and ")
}
return `all of ${items.slice(0, -1).join(", ")} and ${
items[items.length - 1]
}`
}

/**
* Build notes from a rule for rendering the header of the doc file.
*/
export function buildNotesFromRule(rule: RuleModule, isNew: boolean): string[] {
const {
meta: {
fixable,
hasSuggestions,
deprecated,
replacedBy,
docs: { recommended },
},
} = rule
const notes = []

if (deprecated) {
if (replacedBy) {
const replacedRules = replacedBy.map(
(name) => `[astro/${name}](${name}.md) rule`,
)
notes.push(
`- ⚠️ This rule was **deprecated** and replaced by ${formatItems(
replacedRules,
)}.`,
)
} else {
notes.push("- ⚠️ This rule was **deprecated**.")
}
} else if (recommended) {
if (recommended === "base") {
notes.push(
'- ⚙ This rule is included in `"plugin:astro/base"` and `"plugin:astro/recommended"`.',
)
} else {
notes.push('- ⚙ This rule is included in `"plugin:astro/recommended"`.')
}
}
if (fixable) {
notes.push(
"- 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.",
)
}
if (hasSuggestions) {
notes.push(
"- 💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).",
)
}
if (isNew) {
notes.push(
`- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>`,
)
}
return notes
}
5 changes: 2 additions & 3 deletions tools/new-rule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from "path"
import fs from "fs"
import cp from "child_process"
import { renderRuleHeader } from "./lib/doc-renderer"
const logger = console

// main
Expand Down Expand Up @@ -77,9 +78,7 @@ tester.run("${ruleId}", rule as any, loadTestCases("${ruleId}"))
)
fs.writeFileSync(
docFile,
`# (astro/${ruleId})
> description
`${renderRuleHeader({ ruleId: `astro/${ruleId}`, description: "description", notes: [] })}
## 📖 Rule Details
Expand Down
90 changes: 10 additions & 80 deletions tools/update-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,7 @@ import { rules } from "../src/rules"
import type { RuleModule } from "../src/types"
import { getNewVersion } from "./lib/changesets-util"
import { formatAndSave } from "./lib/utils"

//eslint-disable-next-line jsdoc/require-jsdoc -- tools
function formatItems(items: string[]) {
if (items.length <= 2) {
return items.join(" and ")
}
return `all of ${items.slice(0, -1).join(", ")} and ${
items[items.length - 1]
}`
}
import { buildNotesFromRule, renderRuleHeader } from "./lib/doc-renderer"

//eslint-disable-next-line jsdoc/require-jsdoc -- tools
function yamlValue(val: unknown) {
Expand Down Expand Up @@ -60,9 +51,7 @@ class DocFile {
this.filePath = path.join(ROOT, `${rule.meta.docs.ruleName}.md`)
this.content = fs.existsSync(this.filePath)
? fs.readFileSync(this.filePath, "utf8")
: `
`
: "\n\n"
this.since = pickSince(this.content)
}

Expand All @@ -71,74 +60,15 @@ class DocFile {
}

public updateHeader() {
const {
meta: {
fixable,
hasSuggestions,
deprecated,
replacedBy,
docs: { ruleId, description, recommended },
},
} = this.rule
const title = `# ${ruleId}\n\n> ${description}`
const notes = []

if (deprecated) {
if (replacedBy) {
const replacedRules = replacedBy.map(
(name) => `[astro/${name}](${name}.md) rule`,
)
notes.push(
`- ⚠️ This rule was **deprecated** and replaced by ${formatItems(
replacedRules,
)}.`,
)
} else {
notes.push("- ⚠️ This rule was **deprecated**.")
}
} else if (recommended) {
if (recommended === "base") {
notes.push(
'- ⚙ This rule is included in `"plugin:astro/base"` and `"plugin:astro/recommended"`.',
)
} else {
notes.push(
'- ⚙ This rule is included in `"plugin:astro/recommended"`.',
)
}
}
if (fixable) {
notes.push(
"- 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.",
)
}
if (hasSuggestions) {
notes.push(
"- 💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).",
)
}
if (!this.since) {
notes.unshift(
`- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>`,
)
}

// Add an empty line after notes.
if (notes.length >= 1) {
notes.push("", "")
}

const ruleDocs = this.rule.meta.docs
const { ruleId, description } = ruleDocs
const isNewRule = !this.since
const notes = buildNotesFromRule(this.rule, isNewRule)
const headerPattern = /(?:^|\n)#.+\n+[^\n]*\n+(?:- .+\n+)*\n*/u

const header = `\n${title}\n\n${notes.join("\n")}`
if (headerPattern.test(this.content)) {
this.content = this.content.replace(
headerPattern,
header.replace(/\$/g, "$$$$"),
)
} else {
this.content = `${header}${this.content.trim()}\n`
}
const header = renderRuleHeader({ ruleId, description, notes })
this.content = headerPattern.test(this.content)
? this.content.replace(headerPattern, header)
: `${header}${this.content.trim()}\n`

return this
}
Expand Down

0 comments on commit db90a41

Please sign in to comment.