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.
145 lines
3.2 KiB
JavaScript
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,
|
|
}
|