bio-dts
Version:
Generate sane and clean types from JavaScript sources
287 lines (231 loc) • 5.6 kB
JavaScript
import { parse as recastParse, print as recastPrint } from 'recast';
import { Path as PathConstructor } from 'ast-types';
/**
* @template T extends
* @typedef { import('ast-types/lib/path.js').Path<T> } Path
*/
import * as typescriptParser from './parsers/typescript.js';
const DBG = /match/.test(process?.env?.LOG_DEBUG);
const formatOptions = {
useTabs: false,
reuseWhitespace: false,
tabWidth: 2,
quote: /** @type { 'single' } */ ('single')
};
/**
* Return numeric or string keys of the given object.
*
* @param {any} obj
* @return {(string|number)[]}
*/
export function keys(obj) {
return Object.keys(obj).map(k => {
const n = parseInt(k, 10);
return isNaN(n) ? k : n;
});
}
/**
* @param {any} arr
*
* @return {boolean}
*/
export function isArray(arr) {
return Array.isArray(arr);
}
export function isNode(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
export function hasProperty(obj, property) {
return Object.prototype.hasOwnProperty.call(obj, property);
}
/**
* @param { string } code
* @param { { jsx?: boolean } } [parseOptions]
*
* @return {any}
*/
export function parse(code, parseOptions) {
const parser = {
parse: parseOptions?.jsx ? typescriptParser.jsx : typescriptParser.js
};
return recastParse(code, {
parser,
...formatOptions
});
}
export function parseDts(code) {
const parser = {
parse: typescriptParser.dts
};
return recastParse(code, {
parser,
...formatOptions
});
}
export function print(node) {
return recastPrint(node, formatOptions);
}
/**
* @template V
*
* @param {V} node
*
* @return {Path<V>}
*/
export function path(node) {
return new PathConstructor(node);
}
/**
* @param {TemplateStringsArray} strings
* @param {...any} args
*
* @return { (node) => any[][] }
*/
export function matcher(strings, ...args) {
const code = strings.reduce((code, str, i) => {
code += str;
if (args[i]) {
code += args[i];
}
return code;
}, '');
const ast = parse(code);
const body = path(ast).get('program', 'body');
if (body.value.length !== 1) {
throw new Error(`${ code } is more than one statement!`);
}
const expr = body.get(0);
/**
* Match for wildcards:
*
* - block statement with a single $$$
* - expression statement with $$$
* - identifier $$$
* - wrapped as list
*
* @param {any} node
*
* @return {boolean}
*/
function wildcard(node) {
if (isArray(node) && node.length === 1) {
node = node[0];
}
if (node && node.type === 'ExpressionStatement') {
node = node.expression;
}
if (node && node.type === 'Identifier' && node.name === '$$$') {
return true;
}
}
/**
* Matches ${IDENTIFIER} single node.
*
* @param {any} node
*
* @return {any|null}
*/
function ident(node) {
if (node.type === 'Identifier' && /^\$.+/.test(node.name)) {
return node.name.substring(1);
}
return null;
}
/**
* Matches $${IDENTIFIER} many nodes:
*
* - block statement with a single $${IDENT}
* - expression statement with $${IDENT}
* - identifier $${IDENT}
* - wrapped as list
*
* @param {any} node
*
* @return {any|null}
*/
function anyIdent(node) {
if (isArray(node) && node.length === 1) {
node = node[0];
}
if (node && node.type === 'ExpressionStatement') {
node = node.expression;
}
if (node && node.type === 'Identifier' && /^\$\$.+/.test(node.name)) {
return node.name.substring(2);
}
return null;
}
/**
* @template T
* @template V
* @param {Path<T>} node
* @param {Path<V>} expectedNode
* @param {Path<any>[]} results
*
* @return {Path<any>[] | null}
*/
function matchNode(node, expectedNode, results = []) {
if (!node.value) {
DBG && console.log('BAIL no value');
return;
}
const id = ident(expectedNode.value);
if (id) {
results[id] = node;
return results;
}
for (const key of keys(expectedNode.value)) {
if (typeof key === 'string' && [ 'loc', 'start', 'end' ].includes(key)) {
continue;
}
if (!hasProperty(node.value, key)) {
DBG && console.log('BAIL no property', key);
return;
}
const expectedVal = expectedNode.get(key);
const nodeVal = node.get(key);
if (wildcard(expectedVal.value)) {
continue;
}
const id = anyIdent(expectedVal.value);
if (id) {
results[id] = nodeVal;
continue;
}
if (isArray(expectedVal.value)) {
if (
expectedVal.value.some((el, idx) => {
return !matchNode(nodeVal.get(idx), expectedVal.get(idx), results);
})
) {
DBG && console.log('BAIL no array match', key);
return;
}
}
else if (isNode(expectedVal.value)) {
if (!matchNode(nodeVal, expectedVal, results)) {
DBG && console.log('BAIL no match', key);
return;
}
}
else {
if (nodeVal.value !== expectedVal.value) {
DBG && console.log('BAIL no primitive match', key);
return;
}
}
}
return results;
}
return function match(nodes) {
const matches = [];
for (const key of keys(nodes.value)) {
const node = nodes.get(key);
const matched = matchNode(node, expr);
if (matched) {
matches.push([ node, ...matched.slice(1) ]);
}
}
return matches;
};
}