From 7cc34ac425fc63031d45e17330200633fe2970ba Mon Sep 17 00:00:00 2001 From: Joshua T Kalis Date: Mon, 18 Nov 2024 13:32:14 -0500 Subject: [PATCH] start for docblock check --- package.json | 2 + pnpm-lock.yaml | 9 +++ scripts/docblock.mjs | 136 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 scripts/docblock.mjs diff --git a/package.json b/package.json index 0dadfb48..4b508780 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "indexer": "turbo index", "license": "pnpm zx scripts/license.mjs", "lint": "turbo lint", + "postlint": "pnpm zx scripts/docblock.mjs", "lint:fs": "pnpm ls-lint", "test": "turbo test" }, @@ -34,6 +35,7 @@ "@biomejs/biome": "^1.9.3", "@changesets/changelog-git": "^0.2.0", "@changesets/cli": "^2.27.9", + "comment-parser": "^1.4.1", "@ls-lint/ls-lint": "2.3.0-beta.1", "@swc/core": "^1.7.36", "rimraf": "^5.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 858b3970..24423b14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@swc/core': specifier: ^1.7.36 version: 1.9.2(@swc/helpers@0.5.15) + comment-parser: + specifier: ^1.4.1 + version: 1.4.1 rimraf: specifier: ^5.0.10 version: 5.0.10 @@ -2830,6 +2833,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -7820,6 +7827,8 @@ snapshots: commander@4.1.1: {} + comment-parser@1.4.1: {} + confbox@0.1.8: {} consola@3.2.3: {} diff --git a/scripts/docblock.mjs b/scripts/docblock.mjs new file mode 100644 index 00000000..f0a4ccb6 --- /dev/null +++ b/scripts/docblock.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env zx + +/* + * Copyright 2024 Hypergiant Galactic Systems Inc. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { parseFileSync } from '@swc/core'; +import { parse } from 'comment-parser'; +import { fs, glob } from 'zx'; + +const SWC_OPTIONS = { + syntax: 'typescript', + target: 'es2022', +}; + +function exportsReducer(acc, node) { + if (node.type === 'ExportDeclaration') { + if (node.declaration.identifier) { + acc.push(node.declaration.identifier.value); + } else if (node.declaration.declarations) { + for (const inner of node.declaration.declarations) { + acc.push(inner.id.value); + } + } + } + + return acc; +} + +function getFileDetails(path) { + const ast = parseFileSync(path, SWC_OPTIONS); + const source = fs.readFileSync(path, 'utf8'); + + const exports = ast.body.reduce(exportsReducer, []); + const [pragma, ...list] = pragmaParser(source); + + const result = [source]; + + switch (pragma) { + case 'ignore': + result.push( + list[0] === '*' ? [] : exports.filter((name) => !list.includes(name)), + ); + break; + case 'only': + result.push(exports.filter((name) => list.includes(name))); + break; + default: + result.push(exports); + break; + } + + return result; +} + +const pragmaParser = (() => { + const rCleaner = /,\s*/; + const rPragma = /\/\/\s*@export-(ignore|only)(?:\s*\[([^\]]+)\])?/; + const rPrivate = /\/\/\s*__private-exports/i; + + return (src) => { + if (src.match(rPrivate)?.[0]) { + return ['ignore', '*']; + } + + const found = src.match(rPragma); + + if (!found) { + return []; + } + + const pragmas = found + .slice(1) + .reduce((a, b = '*') => [ + a, + ...(b ? b.replace(rCleaner, ',').split(',') : b), + ]); + + if (pragmas[0] === 'only' && pragmas[1] === '*') { + return []; + } + + return pragmas; + }; +})(); + +async function run() { + const noDocblock = ( + await glob(['**/*.{js,ts,tsx,mjs}'], { + ignore: [ + '**/.github/**', + '**/apps/**', + '**/__fixtures__/**', + '**/__mock__/**', + '**/coverage/**', + '**/dist/**', + '**/node_modules/**', + '**/tooling/**', + '**/*.test*', + '**/*.config*', + '**/*.css*', + '**/*.stories*', + ], + }) + ).filter((file) => { + try { + const [source, exports] = getFileDetails(file); + + // has exports and no docblock + return exports.length && !parse(source).length; + } catch (_) { + // ignore non-parsing files + return false; + } + }); + + if (noDocblock.length) { + console.error( + `${noDocblock.length} files missing a docblock:`, + JSON.stringify(noDocblock, null, 4), + ); + + // TODO: enable error-ing once all file are complying + // process.exit(1); + } +} + +await run();