From dea521029e821da7a11a82bbb7585a7162e55dc1 Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Fri, 1 Sep 2023 17:52:44 +0200 Subject: [PATCH] Get a real TypeScript plugin working --- README.md | 6 +++ package.json5 | 2 + src/experiment/arithmeticInfer4.ts | 8 ++++ src/generic/infer.ts | 5 ++ src/plugins/infer4.ts | 75 ++++++++++++++++++++++++++++++ src/plugins/myFirstPlugin.ts | 58 +++++++++++++++++++++++ tsconfig.json | 9 ++-- 7 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 src/experiment/arithmeticInfer4.ts create mode 100644 src/plugins/infer4.ts create mode 100644 src/plugins/myFirstPlugin.ts diff --git a/README.md b/README.md index 6402234..897dd0b 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ None of the experiments (`infer1` and `infer2`) are outputting something useful pnpm experiment:infer1-direct pnpm experiment:infer2 pnpm experiment:infer3 + pnpm experiment:infer4 ### Read more @@ -61,3 +62,8 @@ None of the experiments (`infer1` and `infer2`) are outputting something useful - https://stackoverflow.com/questions/48886508/typechecker-api-how-do-i-find-inferred-type-arguments-to-a-function - https://blog.logrocket.com/using-typescript-transforms-to-enrich-runtime-code-3fd2863221ed/ - https://github.com/itsdouges/typescript-transformer-handbook#transforms + +### Interesting libraries + +- https://github.com/GoogleFeud/ts-macros/ +- https://ts-morph.com diff --git a/package.json5 b/package.json5 index 3c214e5..1f3d6e0 100644 --- a/package.json5 +++ b/package.json5 @@ -9,6 +9,8 @@ 'experiment:infer1-direct': 'ttsc -b && node build/plugins/infer1.js ./src/experiment/arithmeticInfer1.ts', 'experiment:infer2': 'ttsc -b && node build/plugins/infer2.js ./src/experiment/arithmeticInfer2.ts', 'experiment:infer3': 'ttsc -b && node build/plugins/infer3.js ./src/experiment/arithmeticInfer3.js', + 'experiment:infer4': 'ttsc -b && node build/plugins/infer4.js ./src/experiment/arithmeticInfer4.ts', + 'experiment:infer4:plugin': 'ttsc -b', test: 'echo "Error: no test specified" && exit 1', }, keywords: [ diff --git a/src/experiment/arithmeticInfer4.ts b/src/experiment/arithmeticInfer4.ts new file mode 100644 index 0000000..dc2f45a --- /dev/null +++ b/src/experiment/arithmeticInfer4.ts @@ -0,0 +1,8 @@ +import { typed } from '../generic/infer' + +export const square = typed('__infer__', (dep: { + multiply: (a: T, b: T) => T, + unaryMinus: (x: T) => T, // just for the experiment +}): (a: T) => T => + z => dep.multiply(z, z) +) diff --git a/src/generic/infer.ts b/src/generic/infer.ts index ac038dd..92db72d 100644 --- a/src/generic/infer.ts +++ b/src/generic/infer.ts @@ -2,3 +2,8 @@ export function infer(arg: T) : T { console.error('infer should be replaced with runtime type information by a magic TypeScript plugin') return arg } + +export function typed(dep: string, arg: T) : T { + console.error('infer should be replaced with runtime type information by a magic TypeScript plugin') + return arg +} diff --git a/src/plugins/infer4.ts b/src/plugins/infer4.ts new file mode 100644 index 0000000..c5e7834 --- /dev/null +++ b/src/plugins/infer4.ts @@ -0,0 +1,75 @@ +import { readFileSync } from "fs"; +import * as ts from "typescript"; +import { inspect } from 'util' + +export function infer(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { + recurse(sourceFile); + + function recurse(node: ts.Node) { + if (ts.isCallExpression(node)) { + if (ts.isIdentifier(node.expression) && node.expression.escapedText === 'infer') { + const argNode = node.arguments[0] + + if (ts.isArrowFunction(argNode)) { + const text = sourceFile.text.slice(argNode.pos, argNode.end) + console.log('infer', text) + + console.log('AST') + console.log(node) + console.log() + + const returnType = argNode.type.getText(sourceFile) + console.log('returnType', returnType) + // (a: number) => number + + const paramNode = argNode.parameters[0] + + const paramType = paramNode.type.getText(sourceFile) + console.log('paramType', paramType) // (a: number) => number + // { + // multiply: (a: number, b: number) => number, + // unaryMinus: (x: number) => number, // just for the experiment + // } + + const type = typeChecker.getTypeAtLocation(paramNode) + const typeStr = checker.typeToString(type, paramNode, ts.TypeFormatFlags.InTypeAlias) + console.log('paramTypeString', typeStr) + // { multiply: (a: number, b: number) => number; unaryMinus: (x: number) => number; } + } + + } + } + + ts.forEachChild(node, recurse); + } +} + +const fileNames = process.argv.slice(2); +console.log('infer files', fileNames) + +const options = { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ES2022 +} +let program = ts.createProgram(fileNames, options); +const checker = program.getTypeChecker() + +for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + console.log('FILE') + console.log(sourceFile.fileName) + console.log() + + console.log('SOURCE') + console.log(sourceFile.text) + console.log() + + // console.log('AST') + // console.log(inspect(sourceFile, { depth: null, colors: true })) + // console.log() + + console.log('INFER') + infer(sourceFile, checker); + + } +} diff --git a/src/plugins/myFirstPlugin.ts b/src/plugins/myFirstPlugin.ts new file mode 100644 index 0000000..ba2feb6 --- /dev/null +++ b/src/plugins/myFirstPlugin.ts @@ -0,0 +1,58 @@ +import * as ts from 'typescript'; + +const transformer: ts.TransformerFactory = context => { + // TODO: get a reference to the program instance that the plugin is running in instead of creating a new program? + const program = ts.createProgram([], {}) + const checker = program.getTypeChecker() + + return sourceFile => { + // For the experiment we only want to influence a single file + if (!sourceFile.fileName.endsWith('arithmeticInfer4.ts')) { + return sourceFile + } + + const visitor = (node: ts.Node): ts.Node => { + // @ts-ignore + if (ts.isStringLiteral(node) && node.text === '__infer__') { + console.log('STRING LITERAL', node.text) + console.log(node) + + const parentNode = node.parent + console.log('PARENT') + console.log(parentNode) + + // TODO: validate that the parent is indeed a mathjs typed function with deps + + // @ts-ignore + const argNode = parentNode.arguments[1] + const returnType = argNode.type.getText(sourceFile) + console.log('RETURN TYPE') + console.log(returnType) + // (a: number) => number + + const paramNode = argNode.parameters[0] + const paramTypeSrc = paramNode.type.getText(sourceFile) + console.log('PARAM TYPE SRC', paramTypeSrc) + // { + // multiply: (a: number, b: number) => number, + // unaryMinus: (x: number) => number, // just for the experiment + // } + + const type = checker.getTypeAtLocation(paramNode) + const paramType = checker.typeToString(type, paramNode, ts.TypeFormatFlags.InTypeAlias) + console.log('PARAM TYPE STRING', paramType) + // { multiply: (a: number, b: number) => number; unaryMinus: (x: number) => number; } + + const depsAndReturnType = `{ deps: ${paramType}; return: ${returnType} }` + + return ts.factory.createStringLiteral(depsAndReturnType) + } + + return ts.visitEachChild(node, visitor, context); + }; + + return ts.visitNode(sourceFile, visitor); + }; +}; + +export default transformer; diff --git a/tsconfig.json b/tsconfig.json index c761525..50df804 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,10 +8,9 @@ "noImplicitAny": false, "moduleResolution": "Node", "module": "commonjs", - "plugins": [ - { - "transform": "typescript-rtti/dist/transformer" - } - ] + "plugins": [{ + "transform": "./src/plugins/myFirstPlugin.ts", + "type": "raw" + }] } }