Skip to content

Commit

Permalink
feat(linting): check for docblock in public API
Browse files Browse the repository at this point in the history
closes #103
  • Loading branch information
kalisjoshua committed Dec 2, 2024
1 parent c339d9c commit 8fe201f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 1 deletion.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"deps:version-minor": "pnpm taze minor -r",
"deps:version-patch": "pnpm taze patch -r",
"dev": "turbo dev",
"docblocks": "pnpm zx scripts/docblock.mjs",
"format": "pnpm biome format . --write",
"indexer": "turbo index",
"license": "pnpm zx scripts/license.mjs",
Expand All @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions scripts/docblock.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/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 { parse } from 'comment-parser';
import { glob } from 'zx';

import { getFileDetails } from './file-details.mjs';

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);
}
80 changes: 80 additions & 0 deletions scripts/file-details.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 { fs } from 'zx';

import { pragmaParser } from './pragma-parser.mjs';

const SWC_OPTIONS = {
syntax: 'typescript',
target: 'es2022',
};

/** Collect the exported members' names. */
function exportsReducer(acc, member) {
if (member.type === 'ExportDeclaration') {
if (member.declaration.declarations) {
// const, let, var allow for comma separated values
for (const inner of member.declaration.declarations) {
acc.push(inner.id.value);
}
} else {
acc.push(
member.declaration?.identifier?.value || member.declaration?.id?.value,
);
}
}

return acc;
}

/**
* Get the source code of the file and the exported members; taking into
* consideration the pragmas that affect what members will be available in the
* public API:
*
* - `__private-exports`
* - `export-ignore`
* - `export-ignore [x, y, z]`
* - `export-only [a, b, c]`
*
* @returns [string, string[]] // [source, exports]
*/
export function getFileDetails(path, options = {}) {
const ast = parseFileSync(path, {
...SWC_OPTIONS,
...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;
}
50 changes: 50 additions & 0 deletions scripts/pragma-parser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.
*/

const rCleaner = /,\s*/;
const rPragma = /\/\/\s*@export-(ignore|only)(?:\s*\[([^\]]+)\])?/;
const rPrivate = /\/\/\s*__private-exports/i;

/**
* Look for pragmas in the file that would change which exports are considered for the public API:
*
* - `__private-exports`
* - `export-ignore`
* - `export-ignore [x, y, z]`
* - `export-only [a, b, c]`
*
* @returns string[] // [ignore|only, ...members]
*/
export function pragmaParser(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;
}
5 changes: 4 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"persistent": true,
"dependsOn": ["index"]
},
"lint": {},
"docblocks": {},
"lint": {
"dependsOn": ["docblocks"]
},
"bench": {},
"test": {
"dependsOn": ["^build"],
Expand Down

0 comments on commit 8fe201f

Please sign in to comment.