nose-pluto/src/sniffer.ts

106 lines
3.0 KiB
TypeScript

// sniffs the types and params of a file's exports, including const values
import ts from "typescript"
export type Signature = {
type: string,
returnType: string | null,
params: Param[]
}
export type Param = {
name: string,
type: string,
optional: boolean,
rest: boolean,
default: string | null
}
export class SniffError extends Error {
constructor(message: string) {
super(message)
this.name = "TypeError"
Object.setPrototypeOf(this, SniffError.prototype)
}
}
export type ExportInfo =
| { kind: "function", name: string, signatures: Signature[] }
| { kind: "value", name: string, type: string, value: string | null }
let prevProgram: ts.Program | undefined
export async function moduleExports(file: string): Promise<Record<string, ExportInfo>> {
const program = ts.createProgram([file], {
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeNext,
noLib: true,
types: [],
skipDefaultLibCheck: true,
skipLibCheck: true
}, undefined, prevProgram)
prevProgram = program
const checker = program.getTypeChecker()
const sf = program.getSourceFile(file)
if (!sf) throw new SniffError(`File not found: ${file}`)
const moduleSym = (sf as any).symbol as ts.Symbol | undefined
if (!moduleSym) return {}
const exportSymbols = checker.getExportsOfModule(moduleSym)
const result: ExportInfo[] = []
for (const sym of exportSymbols) {
const decl = sym.valueDeclaration ?? sym.declarations?.[0] ?? sf
const type = checker.getTypeOfSymbolAtLocation(sym, decl)
const sigs = checker.getSignaturesOfType(type, ts.SignatureKind.Call)
if (sigs.length > 0) {
result.push({
kind: "function",
name: sym.getName(),
signatures: sigs.map(sig => ({
type: checker.typeToString(type, decl),
returnType: checker.typeToString(checker.getReturnTypeOfSignature(sig), decl),
params: sig.getParameters().map(p => {
const pd: any = p.getDeclarations()?.[0] ?? decl
const pt = checker.getTypeOfSymbolAtLocation(p, pd)
return {
name: p.getName(),
type: checker.typeToString(pt, pd),
optional: !!pd.questionToken || !!pd.initializer,
rest: !!pd.dotDotDotToken,
default: pd.initializer ? pd.initializer.getText() : null
}
})
}))
})
} else {
let value: string | null = null
if (ts.isVariableDeclaration(decl) && decl.initializer) {
value = decl.initializer.getText()
}
result.push({
kind: "value",
name: sym.getName(),
type: checker.typeToString(type, decl),
value
})
}
}
return Object.fromEntries(
result.map(item => [item.name, item])
)
}
if (import.meta.main) {
const path = Bun.argv[2]
if (!path) {
console.error("usage: sniff <path>")
process.exit(1)
}
console.log(await moduleExports(path))
}