Pimp the docs linter (and assorted changes) (#2553)

### Pimp the docs linter (and assorted changes)

 #### `scripts/lint-docs.js`

- Add an optional cache for faster runs
- Add a final report
- Don't return anything from `exec()`
- Cover more files

 #### `scripts/_command.js`

- Look for a "--cache" option

 #### `package.json` scripts

- Added `watch:lint-docs`
- Added `cleanup:lint` to remove the eslint and lint-docs cache files
- Changed `lint:docs` to use the `--cache` option
- Added `test:js` so that we can run the test suite without the linter
- Changed `test` to defer to `test:js`

 #### Actual lint fixes:

- Bad link in a migration guide
- The unicode dashes in the "https://en.wikipedia.org/wiki/Subject–verb–object" are not escaped by marked

### Some more lint-docs pimping

#### `scripts/lint-docs.js`

- some code reorg and cleanup (take a hint from the local coding conventions)
- fix misc bugs
- pass a User-Agent header to the requests
- even nicer reporting

#### `package.json`

- bump the @babel/parser dep to the latest

#### Docs

- tweaks based on lints missed due to previous bugs

### Docs: use the github page for velocity.js, the home page has too many errors.

Co-Authored-By: Isiah Meadows <contact@isiahmeadows.com>
This commit is contained in:
Pierre-Yves Gérardy 2019-12-19 23:40:52 +01:00 committed by GitHub
parent d257025253
commit 4a3a486d80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 182 additions and 62 deletions

View file

@ -18,6 +18,7 @@ process.on("unhandledRejection", function (e) {
module.exports = ({exec, watch}) => {
const index = process.argv.indexOf("--watch")
const useCache = process.argv.indexOf("--cache") >= 0
if (index >= 0) {
process.argv.splice(index, 1)
@ -29,7 +30,7 @@ module.exports = ({exec, watch}) => {
watch()
} else {
Promise.resolve(exec()).then((code) => {
Promise.resolve(exec({useCache})).then((code) => {
if (code != null) process.exitCode = code
})
}

View file

@ -14,24 +14,22 @@ const request = require("request-promise-native")
class LintRenderer extends marked.Renderer {
constructor(file) {
super()
this._file = file
this._dir = path.dirname(file)
this._context = undefined
this._code = undefined
this._lang = undefined
this._error = undefined
this._awaiting = []
this._warnings = []
this._errors = []
}
_emitTolerate(...data) {
let str = data.join("\n")
if (str.endsWith("\n")) str = str.slice(0, -1)
console.log(`${this._file} - ${str}\n${"-".repeat(60)}`)
_addWarning(...data) {
this._warnings.push(formatMessage(...data))
}
_emit(...data) {
this._emitTolerate(...data)
process.exitCode = 1
_addError(...data) {
this._errors.push(formatMessage(...data))
}
_block() {
@ -42,31 +40,48 @@ class LintRenderer extends marked.Renderer {
// Don't fail if something byzantine shows up - it's the freaking
// internet. Just log it and move on.
const httpError = (e) =>
this._emitTolerate(`http error for ${href}`, e.message)
this._addWarning(`http error for ${href}`, e.message)
// Prefer https: > http: where possible, but allow http: when https: is
// inaccessible.
if ((/^https?:\/\//).test(href)) {
const url = href.replace(/#.*$/, "")
this._awaiting.push(request.head(url).then(() => {
const isHTTPS = href.startsWith("https:")
const isHTTPS = href.startsWith("https:")
// pass along realistic headers, some sites (i.e. the IETF) return a 403 otherwise.
const headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0",
}
// some more headers if more were ever needed (from my local Firefox)
// "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
// "Accept-Language": "en-US,en;q=0.5",
// "Accept-Encoding": "gzip, deflate, br",
// "DNT": "1",
// "Connection": "keep-alive",
// "Upgrade-Insecure-Requests": "1",
// "Pragma": "no-cache",
// "Cache-Control": "no-cache"
this._awaiting.push(request.head(url, {headers}).then(() => {
if (!isHTTPS) {
return request.head(`https:${url.slice(7)}`).then(
() => this._emit("change http: to https:"),
return request.head(`https:${url.slice(7)}`, {headers}).then(
() => this._addError("change http: to https:"),
() => { /* ignore inner errors */ }
)
}
}, (e) => {
if (e.statusCode === 404) {
this._emit(`broken external link: ${href}`)
this._addError(`broken external link: ${href}`)
}
else {
if (
e.error.code === "ERR_TLS_CERT_ALTNAME_INVALID" &&
href.startsWith("https://")
isHTTPS && (
e.error.code === "ERR_TLS_CERT_ALTNAME_INVALID" ||
(/ssl/i).test(e.message)
)
) {
return request.head(`http:${url.slice(6)}`).then(
() => this._emit(`change ${href} to use http:`),
return request.head(`http:${url.slice(6)}`, {headers}).then(
() => this._addError(`change ${href} to use http:`),
// ignore inner errors
() => httpError(e)
)
@ -80,7 +95,7 @@ class LintRenderer extends marked.Renderer {
if (exec != null) {
const resolved = path.resolve(this._dir, exec[1])
this._awaiting.push(fs.access(resolved).catch(() => {
this._emit(`broken internal link: ${href}`)
this._addError(`broken internal link: ${href}`)
}))
}
}
@ -89,6 +104,8 @@ class LintRenderer extends marked.Renderer {
code(code, lang) {
this._code = code
this._lang = lang
this._error = null
if (!lang || lang === "js" || lang === "javascript") {
try {
// Could be within any production.
@ -115,7 +132,7 @@ class LintRenderer extends marked.Renderer {
if (!this._lang) {
// TODO: ensure all code blocks have tags, and check this in CI.
if (this._error == null) {
this._emit(
this._addError(
"Code block possibly missing `javascript` language tag",
this._block(),
)
@ -123,7 +140,7 @@ class LintRenderer extends marked.Renderer {
try {
JSON.parse(this._code)
this._emit(
this._addError(
"Code block possibly missing `json` language tag",
this._block(),
)
@ -135,9 +152,9 @@ class LintRenderer extends marked.Renderer {
}
_ensureCodeIsSyntaticallyValid() {
if (!this.lang || !(/^js$|^javascript$/).test(this._lang)) return
if (!this._lang || !(/^js$|^javascript$/).test(this._lang)) return
if (this._error != null) {
this._emit(
this._addError(
"JS code block has invalid syntax", this._error.message,
this._block()
)
@ -145,28 +162,124 @@ class LintRenderer extends marked.Renderer {
}
_ensureCommentStyle() {
if (!this.lang || !(/^js$|^javascript$/).test(this._lang)) return
if (!this._lang || !(/^js$|^javascript$/).test(this._lang)) return
if ((/(^|\s)\/\/[\S]/).test(this._code)) {
this._emit("Comment is missing a preceding space", this._block())
this._addError("Comment is missing a preceding space", this._block())
}
}
}
async function getFileInfo(file) {
const {size, mtime} = await fs.stat(file)
const timestamp = Number(mtime)
return {size, timestamp}
}
function report(file, data, totals, nextCache) {
const {_warnings, _errors} = data;
if (_warnings.length + _errors.length > 0) {
console.log("- ".repeat(file.length/2 + 1))
console.log(file)
console.log("- ".repeat(file.length/2 + 1) + "\n")
if (_errors.length > 0) {
process.exitCode = 1
const s = _errors.length > 1 ? "s " : " -"
console.log(`-- ${_errors.length} Error${s}----------`)
_errors.forEach((msg) => console.log(`\n${msg}`))
console.log("\n")
}
if (_warnings.length > 0) {
const s = _warnings.length > 1 ? "s " : " -"
console.log(`-- ${_warnings.length} Warning${s}--------`)
_warnings.forEach((msg) => console.log(`\n${msg}`))
console.log("\n")
}
if (totals != null) {
totals.errors += _errors.length
totals.warnings += _warnings.length
}
}
if (nextCache != null) nextCache[file] = data
}
function formatMessage(...data) {
let str = data.join("\n")
if (str.endsWith("\n")) str = str.slice(0, -1)
return str
}
exports.lintOne = lintOne
async function lintOne(file) {
// `cache` and `nextCache` are only passed from `lintAll()`, not when watching
async function lintOne(file, totals, cache, nextCache) {
const contents = await fs.readFile(file, "utf-8")
// check for nextCache, because cache will be undefined the first time the linter runs
const {size, timestamp} = (nextCache != null) ? await getFileInfo(file) : {}
if (cache != null && cache[file] != null) {
const cached = cache[file]
if (
size === cached.size &&
timestamp === cached.timestamp &&
cached._errors.length + cached._warnings.length === 0
) {
report(file, cached, totals, nextCache)
return
}
}
const renderer = new LintRenderer(file)
marked(contents, {renderer})
return Promise.all(renderer._awaiting)
return Promise.all(renderer._awaiting).then(() => {
const {_warnings, _errors} = renderer
report(file, {_warnings, _errors, size, timestamp}, totals, nextCache)
})
}
const cachePath = path.join(process.cwd(), ".lint-docs-cache")
async function loadCache() {
try {
const source = await fs.readFile(cachePath, "utf-8")
try {
return JSON.parse(source)
} catch (e) {
console.error(e)
return
}
} catch (e) {
return
}
}
function saveCache(nextCache) {
return fs.writeFile(cachePath, JSON.stringify(nextCache), "utf-8")
}
function finalReport(totals) {
const buffer = []
if (totals.errors > 0) {
buffer.push(`${totals.errors} error${totals.errors > 1 ? "s" : ""}`)
}
if (totals.warnings > 0) {
buffer.push(`${totals.warnings} warning${totals.warnings > 1 ? "s" : ""}`)
}
if (buffer.length > 0) console.log(`\n${buffer.join(", ")} found in the docs\n`)
else console.log("The docs are in good shape!\n")
}
exports.lintAll = lintAll
function lintAll() {
return new Promise((resolve, reject) => {
const glob = new Glob(path.resolve(__dirname, "../docs/**/*.md"), {
async function lintAll({useCache}) {
const cache = useCache ? await loadCache() : null
const totals = {
errors: 0,
warnings: 0,
}
// always populate the cache, even if we don't read from it
const nextCache = {}
await new Promise((resolve, reject) => {
const glob = new Glob(path.resolve(__dirname, "../**/*.md"), {
ignore: [
"**/change-log.md",
"**/migration-*.md",
"**/migration-v02x.md",
"**/node_modules/**",
],
nodir: true,
@ -174,12 +287,15 @@ function lintAll() {
const awaiting = []
glob.on("match", (file) => {
awaiting.push(lintOne(file))
awaiting.push(lintOne(file, totals, cache, nextCache))
})
glob.on("error", reject)
glob.on("end", () => resolve(Promise.all(awaiting)))
})
finalReport(totals)
await saveCache(nextCache)
// don't return anything so that _command.js picks up the errorCode.
}
/* eslint-disable global-require */
@ -188,10 +304,10 @@ if (require.main === module) {
exec: lintAll,
watch() {
require("chokidar")
.watch(path.resolve(__dirname, "../docs/**/*.md"), {
ignore: [
.watch(path.resolve(__dirname, "../**/*.md"), {
ignored: [
"**/change-log.md",
"**/migration-*.md",
"**/migration-v02x.md",
"**/node_modules/**",
],
})