From c8d47a4be20f422a30eb3a577d2431d1f25452b6 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Wed, 21 Feb 2024 14:29:10 -0500 Subject: [PATCH] Thought about bringing some more commands over but too complex. --- commands/codedocs.js | 345 ------------------------------------------- commands/coverage.js | 105 ------------- 2 files changed, 450 deletions(-) delete mode 100644 commands/codedocs.js delete mode 100644 commands/coverage.js diff --git a/commands/codedocs.js b/commands/codedocs.js deleted file mode 100644 index 0b9601b..0000000 --- a/commands/codedocs.js +++ /dev/null @@ -1,345 +0,0 @@ -// you may not need all of these but they come up a lot -import fs from "fs"; -import assert from "assert"; -import logging from '../lib/logging.js'; -import { mkdir_to, glob } from "../lib/builderator.js"; -import { create_renderer } from "../lib/docgen.js"; -import path from "path"; -import slugify from "slugify"; -import * as acorn from "acorn"; -import * as acorn_walk from "acorn-walk"; - -const log = logging.create(import.meta.url); - -export const description = "Describe your command here." - -// your command uses the npm package commander's options format -export const options = [ - ["--quiet", "Don't output the stats at the end."], - ["--readme", "README file to use as the initial page", "README.md"] -] - -// example of a positional argument, it's the 1st argument to main -export const argument = ["", "source input globs"]; - -// put required options in the required variable -export const required = [ - ["--output ", "Save to file rather than stdout."], -] - -const RENDERER = create_renderer(); -const CAPS_WORDS = ["BUG", "TODO", "WARNING", "FOOTGUN", "DEPRECATED"]; -const STATS = {total: 0, docs: 0, undoc: 0}; - -const slug = (instring) => slugify(instring, { lower: true, strict: true, trim: true}); - -/* - Strips 1 leading space from the comments, or the \s\* combinations - in traditional documentation comments. - - If we strip more it'll mess up formatting in markdown for indentation - formats. Weirdly Remarkable seems to be able to handle leading spaces pretty - well so only need to remove one space or \s\* combinations like with - traditional comment docs. -*/ -const render_comment = (comment) => { - const lines = comment.split(/\n/).map(l => l.replace(/^(\s*\*\s?|\s)/, '')); - return RENDERER.render(lines.join("\n")); -} - -/* Handy function for checking things are good and aborting. */ -const check = (test, fail_message) => { - if(!test) { - log.error(fail_message); - process.exit(1); - } -} - -const dump = (obj) => { - return JSON.stringify(obj, null, 4); -} - -class ParseWalker { - constructor(comments, code) { - this.comments = comments; - this.exported = []; - this.code = code; - } - - handle_class(root) { - const new_class = { - isa: "class", - slug: slug(root.declaration.id.name), - name: root.declaration.id.name, - line_start: root.loc.start.line, - methods: [], - } - - acorn_walk.simple(root, { - ClassDeclaration: (cls_node) => { - assert(cls_node.id.name === new_class.name, "Name of class changed!"); - new_class.range = [cls_node.start, cls_node.body.start]; - this.add_export(cls_node.id, new_class); - }, - - MethodDefinition: (meth_node) => { - const new_method = { - isa: "method", - static: meth_node.static, - async: meth_node.value.async, - generator: meth_node.value.generator, - slug: slug(`${new_class.name}-${meth_node.key.name}`), - name: meth_node.key.name, - line_start: meth_node.loc.start.line, - range: [meth_node.start, meth_node.value.body.start], - params: this.handle_params(meth_node.value.params), - comment: this.find_comment(meth_node.loc.start.line), - } - - this.has_CAPS(new_method); - - new_method.code = this.slice_code(new_method.range); - - this.update_stats(new_method); // methods can't go through add_export - new_class.methods.push(new_method); - } - }); - } - - handle_params(param_list) { - const result = []; - - for(let param of param_list) { - acorn_walk.simple(param, { - Identifier: (_node) => { - result.push({isa: "identifier", name: _node.name}); - }, - - AssignmentPattern: (_node) => { - result.push({ - isa: "assignment", - name: _node.left.name, - right: { - type: _node.right.type.toLowerCase(), - raw: _node.right.raw, - value: _node.right.value, - name: _node.right.name, - }, - }); - } - }); - } - - return result; - } - - /* - Used to add information when something is mentioned in the - comment like BUG, TODO, etc. - */ - has_CAPS(exp) { - if(exp.comment) { - exp.caps = CAPS_WORDS.filter(phrase => exp.comment.includes(phrase)); - } else { - exp.caps = []; - } - } - - update_stats(exp) { - STATS.total += 1; - - if(exp.comment) { - STATS.docs += 1; - } else { - STATS.undoc += 1; - } - } - - add_export(id, exp) { - exp.name = id.name; - exp.slug = exp.slug ? exp.slug : slug(id.name); - exp.line_start = id.loc.start.line; - exp.comment = this.find_comment(exp.line_start); - this.has_CAPS(exp); - exp.code = this.slice_code(exp.range); - this.exported.push(exp); - - this.update_stats(exp); - } - - slice_code(range) { - return this.code.slice(range[0], range[1]); - } - - handle_arrow_func(id, arrow) { - this.add_export(id, { - isa: "function", - async: arrow.async, - generator: arrow.generator, - expression: arrow.expression, - range: [id.start, arrow.body.start], - params: this.handle_params(arrow.params), - }); - } - - handle_variable(root) { - const declare = root.declaration.declarations[0]; - const id = declare.id; - const _node = declare.init; - const init_is = declare.init.type; - - if(init_is === "ArrowFunctionExpression") { - this.handle_arrow_func(id, declare.init); - } else { - this.add_export(id, { - isa: _node.type.toLowerCase(), - value: _node.value, - range: declare.range, - raw: _node.raw - }); - } - } - - /* - Find the nearest comment to this line, giving - about 2 lines of slack. - */ - find_comment(line) { - for(let c of this.comments) { - const distance = c.end - line; - if(!c.found && distance == -1) { - c.found = true; - return render_comment(c.value); - } - } - - return undefined; - } - - - /* - Returns the first comment as the file's main doc comment, or undefined if there isn't one. - */ - file_comment() { - const comment = this.comments[0]; - - if(comment && comment.start === 1) { - // kind of a hack, but find_comment will find this now - return this.find_comment(comment.end + 1); - } else { - return undefined; - } - } - - handle_export(_node) { - switch(_node.declaration.type) { - case "ClassDeclaration": - this.handle_class(_node); - break; - case "VariableDeclaration": { - this.handle_variable(_node); - break; - } - default: - console.log(">>>", _node.declaration.type); - } - } -} - -const parse_source = (source) => { - const code = fs.readFileSync(source); - - let comments = []; - - const acorn_opts = { - sourceType: "module", - ecmaVersion: "2023", - locations: true, - sourceFile: source, - ranges: true, - onComment: comments - } - - const parsed = acorn.parse(code, acorn_opts); - - comments = comments.filter(c => c.type === "Block").map(c => { - return { - start: c.loc.start.line, - end: c.loc.end.line, - value: c.value, - type: "comment", - found: false, - } - }); - - const walker = new ParseWalker(comments, code.toString()); - // acorn is stupid and they grab a reference to the functions so that _removes_ - // this from the object, instead of just...calling walker.function() like a normal person - acorn_walk.simple(parsed, { - ExportNamedDeclaration: (_node) => walker.handle_export(_node), - }); - - let comment = walker.file_comment(); - - return { - // normalize to / even on windows - source: source.replaceAll("\\", "/"), - // find the first comment for the file's comment - comment, - exports: walker.exported, - orphan_comments: walker.comments.filter(c => !c.found) - }; -} - -const normalize_name = (fname) => { - const no_slash = fname.replaceAll("\\", "/"); - - if(fname.startsWith("./")) { - return no_slash.slice(2); - } else if(fname.startsWith("/")) { - return no_slash.slice(1); - } else { - return no_slash; - } -} - - -export const main = async (source_globs, opts) => { - const index = {}; - mkdir_to(opts.output); - - for(let source of source_globs) { - const source_list = glob(source); - - for(let fname of source_list) { - const result = parse_source(fname); - - const target = `${path.join(opts.output, fname)}.json`; - mkdir_to(target); - fs.writeFileSync(target, dump(result)); - - const name = normalize_name(fname); - - index[name] = result.exports.map(e => { - return {isa: e.isa, name: e.name}; - }); - } - } - - // now write the grand index - const index_name = path.join(opts.output, "index.json"); - fs.writeFileSync(index_name, dump(index)); - - // render the README.md to the initial docs - const readme_name = path.join(opts.output, "index.html"); - const md_out = RENDERER.render(fs.readFileSync(opts.readme).toString()); - fs.writeFileSync(readme_name, md_out); - - const percent = Math.floor(100 * STATS.docs / STATS.total); - - if(!opts.quiet) { - console.log(`Total ${STATS.total}, ${percent}% documented (${STATS.docs} docs vs. ${STATS.undoc} no docs).`); - } - - process.exit(0); -} diff --git a/commands/coverage.js b/commands/coverage.js deleted file mode 100644 index c46499b..0000000 --- a/commands/coverage.js +++ /dev/null @@ -1,105 +0,0 @@ -import libCoverage from 'istanbul-lib-coverage'; -import libReport from 'istanbul-lib-report'; -import reports from 'istanbul-reports'; -import { glob } from "../lib/builderator.js"; -import fs from "fs"; -import v8toIstanbul from 'v8-to-istanbul'; -import assert from "assert"; -import url from "url"; -import normalize from "normalize-path"; - -export const description = "Takes the output of a nv8 coverage directory and generates a report."; - -export const argument = ["", "coverage directory"]; - -export const main = async (coverage_dir) => { - const covdir = normalize(coverage_dir); - const covpattern = `${covdir}/**/*.json`; - console.log(`Searching ${covpattern} for coverage files...`); - - const covfiles = glob(covpattern); - console.log(`Found ${covfiles.length} .json files in ${covdir}`); - - const coverage = {}; - - const excludes = [ - "node_modules", - "secrets", - "/net" - ]; - - for(const fname of covfiles) { - const data = JSON.parse(fs.readFileSync(fname)); - - // test removing just node modules - data.result = data.result.filter(x => { - if(x.url) { - // we need to do surgery on the URL because node is bad at them - let pathname = url.parse(x.url).pathname; - - // fix the URL and turn it into a file name - if(!pathname) { - return false; - } else if(pathname.startsWith("/C:")) { - // why does url not parse windows paths right? - // remove the leading / so it's parsed correctly - x.url = pathname.slice(1); - } else { - x.url = pathname; - } - - const excluded = excludes.filter(e => x.url.includes(e)); - return excluded.length === 0 && fs.existsSync(x.url); - } else { - return false; - } - }); - - // looks good, save it - if(data.result.length > 0) { - coverage[fname] = data; - } - } - - const coverageMap = libCoverage.createCoverageMap(); - console.log("After filtering, found count is:", Object.entries(coverage).length); - - for(const [fname, data] of Object.entries(coverage)) { - for(const entry of data.result) { - let converter; - const pathname = url.parse(entry.url).pathname - assert(fs.existsSync(pathname), `coverage entry in ${fname} contains ${entry.url} that doesn't exist but should`); - - converter = v8toIstanbul(pathname, 0, {source: entry.source}, path => { - const excluded = excludes.filter(e => path.includes(e)); - return excluded.length > 0; - }); - - try { - await converter.load(); - converter.applyCoverage(entry.functions); - coverageMap.merge(converter.toIstanbul()); - } catch(error) { - console.error(error, "load", entry.url); - } - } - } - - const watermarks = undefined; // used in check coverage ignored here - - const context = libReport.createContext({ - dir: "coverage", - watermarks, - coverageMap - }); - - ["text","html"].forEach(format => { - reports.create(format, { - skipEmpty: false, - skipFull: true, - maxCols: 100 - }).execute(context); - }); - - process.exit(0); -}