mithril-vndb/scripts/_lint-docs/lint-code.js
Claudia Meadows 0d095d1373
Rewrite docs linter
1. I want to set the stage to deal with #2898 properly.
2. `request` was deprecated years ago. Decided that it's better to just
   move to native Node.js APIs in its place.
3. `glob` was outdated, and it's easier to just toss it than to upgrade
   across a major version.
4. I switched to using Marked's "lexer" directly so I'm not fussing
   with the complexity of renderers. This of course necessitated a more
   complex file processor as its "lexer" is really an AST parser.

I also decided to go a few steps further:
- Drop the cache to simplify everything. I might reverse this later,
  but just caching URLs per-page should be enough to prevent the world
  from crashing down.
- Drop some more dependencies, so I don't have to come back to this
  later nearly as quickly.
- Upgrade to a more modern language version in the scripts.
- Update Marked. It was super outdated.
- Add line and column numbers to the warnings. That took quite a bit of
  work, thanks to a missing Marked feature plus a bug in Marked.
2024-09-23 04:54:17 -07:00

145 lines
3.2 KiB
JavaScript

"use strict"
// Accept just about anything by using Babel's parser.
const babelParser = require("@babel/parser")
function getJsonError(code) {
try {
JSON.parse(code)
return undefined
} catch (e) {
return e
}
}
/** Returns `undefined` or an error */
function getBabelError(code, asTypeScript) {
// Could be within any production.
/** @type {babelParser.ParserPlugin[]} */
const plugins = [
"bigInt",
"asyncGenerators",
"classPrivateMethods",
"classPrivateProperties",
"classProperties",
"dynamicImport",
"logicalAssignment",
"nullishCoalescingOperator",
"numericSeparator",
"objectRestSpread",
"optionalCatchBinding",
"optionalChaining",
"topLevelAwait",
"jsx",
]
if (asTypeScript) {
plugins.push("typescript")
}
try {
babelParser.parse(code, {
sourceType: "unambiguous",
allowReturnOutsideFunction: true,
allowAwaitOutsideFunction: true,
allowSuperOutsideMethod: true,
allowUndeclaredExports: true,
plugins,
})
return undefined
} catch (e) {
return e
}
}
/**
* @typedef LangEntry
* @property {string} name
* @property {undefined | RegExp} unspacedComment
* @property {(code: string) => undefined | Error} getError
*/
/** @type {Map<string, string | LangEntry>} */
const recognizedLangTags = new Map([
["js", {
name: "JavaScript",
unspacedComment: /(^|\s)\/\/\S/g,
getError: (code) => getBabelError(code, false),
}],
["ts", {
name: "TypeScript",
unspacedComment: /(^|\s)\/\/\S/g,
getError: (code) => getBabelError(code, true),
}],
["json", {
name: "JSON",
unspacedComment: undefined,
getError: getJsonError,
}],
["javascript", "js"],
["typescript", "ts"],
])
/**
* @param {undefined | string} lang
* @returns {undefined | LangEntry}
*/
function lookupLang(lang) {
while (typeof lang === "string") {
lang = recognizedLangTags.get(lang)
}
return lang
}
function lintCodeIsHighlightable(codeErrors, lang) {
// We only care about what's not tagged here.
if (lang === "") {
// TODO: ensure all code blocks have tags, and check this in CI.
const langTags = []
for (const [tag, getError] of recognizedLangTags) {
if (typeof getError === "function" && !getError(tag)) {
langTags.push(tag)
}
}
if (langTags.length === 1) {
codeErrors.push(`Code block possibly missing \`${langTags[0]}\` language tag.`)
} else if (langTags.length !== 0) {
codeErrors.push([
"Code block possibly missing a language tag. Possible tags that could apply:",
...langTags.map((tag) => `- ${tag}`),
].join("\n"))
}
}
}
function lintCodeIsSyntaticallyValid(codeErrors, langEntry, error) {
if (error) {
codeErrors.push(`${langEntry.name} code block has invalid syntax: ${error.message}`)
}
}
function lintCodeCommentStyle(codeErrors, langEntry, code) {
if (langEntry?.unspacedComment?.test(code)) {
codeErrors.push("Comment is missing a preceding space.")
}
}
function getCodeLintErrors(code, lang) {
const langEntry = lookupLang(lang)
const error = langEntry?.getError(code)
const codeErrors = []
lintCodeIsHighlightable(codeErrors, lang)
lintCodeIsSyntaticallyValid(codeErrors, langEntry, error)
lintCodeCommentStyle(codeErrors, langEntry, code)
return codeErrors
}
module.exports = {
getCodeLintErrors,
}