Skip to content

Instantly share code, notes, and snippets.

@ochafik
Last active February 3, 2017 13:26
Show Gist options
  • Select an option

  • Save ochafik/b59752d6e109684b04af0e0a12346bb9 to your computer and use it in GitHub Desktop.

Select an option

Save ochafik/b59752d6e109684b04af0e0a12346bb9 to your computer and use it in GitHub Desktop.
Copy of github.com/ochafik/typer for the purpose of examination by Google

typer

Adds flow / typescript types to JavaScript files

Typer adds {TypeScript, Flow, Closure} types to JavaScript programs using iterative type propagation and the TypeScript Language Services.

Example

input.js:

function f(x) {
  return x + 1;
}

function g(x, o) {
  if (o.addValue) {
    return f(x) + o.value;
  }
  return o.name == 'default' ? x : 'y';
}
function gg(x, y) {
  var v = gg(x, y);
  return v;
}

output.ts:

function f(x: number) {
  return x + 1;
}

function g(x: number, o: {addValue: boolean, value: number, name: string}) {
  if (o.addValue) {
    return f(x) + o.value;
  }
  return o.name == 'default' ? x : 'y';
}

function gg(x: number, o: {addValue: boolean, value: number, name: string}) {
  var v: string | number = g(x, y);
  return v;
}

Run it

  • Clone this repo
  • Run npm i
  • Run node build/main.js <your .js files>
import * as ts from "typescript";
export type Signature = {
returnType?: ts.Type,
argTypes: ts.Type[]
// argTypesAndNames: [ts.Type, string[]][]
};
export class CallConstraints {
constructor(
private createConstraints: () => TypeConstraints,
public readonly returnType: TypeConstraints = createConstraints(),
public readonly argTypes: TypeConstraints[] = []) {}
private ensureArity(arity: number) {
for (let i = 0; i < arity; i++) {
if (this.argTypes.length <= i) {
this.argTypes.push(this.createConstraints());
}
}
}
getArgType(index: number) {
this.ensureArity(index + 1);
return this.argTypes[index];
}
};
export class TypeConstraints {
private fields = new Map<string, TypeConstraints>();
private types: ts.Type[] = [];
private callConstraints?: CallConstraints;
private flags: ts.TypeFlags = 0;
constructor(
private services: ts.LanguageService,
private checker: ts.TypeChecker,
public initialType?: ts.Type) {
if (initialType) {
this.isType(initialType);
}
}
private createConstraints(initialType?: ts.Type): TypeConstraints {
return new TypeConstraints(this.services, this.checker, initialType);
}
get isPureFunction(): boolean {
return this.callConstraints != null &&
this.flags == 0 &&
this.fields.size == 0 &&
this.types.length == 0;
}
getFieldConstraints(name: string): TypeConstraints {
let constraints = this.fields.get(name);
if (!constraints) {
this.fields.set(name, constraints = this.createConstraints());
}
return constraints;
}
isType(type: ts.Type) {
// type.
// if (type.flags & ts.TypeFlags.Any) {
// return;
// }
if (type.flags & ts.TypeFlags.NumberLiteral) {
this.isNumber();
return;
}
const sym = type.getSymbol();
if (sym && sym.getName() != '__type') {
// console.log(`SYMBOL(${this.checker.typeToString(type)}) = ${sym && this.checker.symbolToString(sym)}`);
this.types.push(type);
return;
}
for (const sig of type.getCallSignatures()) {
const params = sig.getDeclaration().parameters;
const callConstraints = this.getCallConstraints();
callConstraints.returnType.isType(sig.getReturnType());
params.forEach((param, i) => {
const paramType = this.checker.getTypeAtLocation(param);
callConstraints!.getArgType(i).isType(paramType);
});
}
for (const prop of type.getProperties()) {
this.getFieldConstraints(this.checker.symbolToString(prop))
.isType(this.checker.getTypeOfSymbolAtLocation(prop, prop.getDeclarations()[0]));
}
this.flags |= type.flags;
}
getCallConstraints(): CallConstraints {
if (!this.callConstraints) {
this.callConstraints = new CallConstraints(() => this.createConstraints());
}
return this.callConstraints;
}
isNumber() {
this.flags |= ts.TypeFlags.Number;
}
isNullable() {
this.flags |= ts.TypeFlags.Null;
}
isVoid() {
this.flags |= ts.TypeFlags.Void;
}
isBooleanLike() {
this.flags |= ts.TypeFlags.BooleanLike;
}
private typeToString(t: ts.Type | null): string | null {
return t == null ? null : this.checker.typeToString(t);
}
private argsListToString(types: string[]): string {
return types.map((t, i) => `arg${i + 1}: ${t || 'any'}`).join(', ');
}
resolveCallableArgListAndReturnType(): [string, string] | undefined {
return this.callConstraints &&
[
this.argsListToString(
this.callConstraints.argTypes.map(c => c.resolve() || 'any')),
this.callConstraints.returnType.resolve() || 'any'
];
}
resolve(): string | null {
const union: string[] = [];
const members: string[] = [];
for (const [name, constraints] of this.fields) {
if (constraints.isPureFunction) {
const [argList, retType] = constraints!.resolveCallableArgListAndReturnType()!;
members.push(`${name}(${argList}): ${retType}`);
} else {
members.push(`${name}: ${constraints.resolve() || 'any'}`);
}
}
// const signaturesSeen = new Set<string>();
if (this.callConstraints) {
const [argList, retType] = this.resolveCallableArgListAndReturnType()!;
const sigSig = `(${argList})${retType}`;
if (members.length > 0) {
members.push(`(${argList}): ${retType}`)
} else {
union.push(`(${argList}) => ${retType}`);
}
}
for (const type of this.types) {
union.push(this.checker.typeToString(type));
}
const typesFlags = this.types.reduce((flags, type) => flags | type.flags, 0);
const missingFlags = this.flags & ~typesFlags;
if (missingFlags & ts.TypeFlags.Number) {
union.push('number');
}
if (missingFlags & ts.TypeFlags.Null) {
union.push('null');
} else if (missingFlags & ts.TypeFlags.BooleanLike) {
union.push('boolean');
}
if (members.length > 0) {
union.push('{' + members.join(', ') + '}');
}
// Skip void if there's any other type.
if (union.length == 0 && (missingFlags & ts.TypeFlags.Void)) {
union.push('void');
}
const result = union.length == 0 ? null : union.join(' | ');
// console.log(`result = "${result}" (members = [${members}], types = [${this.types.map(t => this.checker.typeToString(t))}], flags = ${this.flags}, missingFlags = ${missingFlags}`);
return result;
}
}
import * as ts from "typescript";
export type VersionedFile = Readonly<{
version: number,
content: string,
originalContent: string,
patchHistory: ReadonlyArray<ReadonlyArray<ts.TextChange>>,
}>;
export function makeVersionedFile(content: string): VersionedFile {
return {
version: 0,
content: content,
originalContent: content,
patchHistory: []
};
}
export function updateVersionedFile(file: VersionedFile, changes: ts.TextChange[]): VersionedFile {
const inverseSortedChanges = [...changes].sort((a, b) => b.span.start - a.span.start);
let content = file.content;
for (const change of inverseSortedChanges) {
content = content.slice(0, change.span.start) + change.newText + content.slice(change.span.start + change.span.length)
}
return {
version: file.version + 1,
content: content,
originalContent: file.originalContent,
patchHistory: [...file.patchHistory, inverseSortedChanges]
};
}
import * as ts from "typescript";
import {AddChangeCallback} from './language_service_reactor';
export function format(fileNames: string[], services: ts.LanguageService, addChange: AddChangeCallback) {
for (const fileName of fileNames) {
services.getFormattingEditsForDocument(fileName, formattingOptions).forEach(c => addChange(fileName, c));
}
}
const formattingOptions: ts.FormatCodeOptions = {
IndentStyle: ts.IndentStyle.Smart,
IndentSize: 2,
TabSize: 2,
BaseIndentSize: 2,
ConvertTabsToSpaces: true,
NewLineCharacter: '\n',
InsertSpaceAfterCommaDelimiter: true,
InsertSpaceAfterSemicolonInForStatements: true,
InsertSpaceBeforeAndAfterBinaryOperators: true,
InsertSpaceAfterConstructor: false,
InsertSpaceAfterKeywordsInControlFlowStatements: true,
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: true,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false,
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: true,
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: true,
InsertSpaceAfterTypeAssertion: false,
InsertSpaceBeforeFunctionParenthesis: false,
PlaceOpenBraceOnNewLineForFunctions: false,
PlaceOpenBraceOnNewLineForControlBlocks: false,
}
import * as ts from "typescript";
import {AddChangeCallback, ReactorCallback} from './language_service_reactor';
import {TypeConstraints, CallConstraints} from './constraints';
export const infer: ReactorCallback = (fileNames, services, addChange) => {
const allConstraints = new Map<ts.Symbol, TypeConstraints>();
const program = services.getProgram();
const checker = program.getTypeChecker();
for (const sourceFile of program.getSourceFiles()) {
if (fileNames.indexOf(sourceFile.fileName) >= 0) {
ts.forEachChild(sourceFile, n => visit(sourceFile.fileName, n));
}
}
for (const [sym, constraints] of allConstraints) {
const resolved = constraints.resolve();
const initial = constraints.initialType && typeToString(constraints.initialType);
if (resolved == null || resolved == initial) {
continue;
}
// console.log(`CONSTRAINTS for ${checker.symbolToString(sym)}: ${resolved}`);
let [decl] = sym.getDeclarations();
// console.log(`${decl.getFullText()}: ${newText}`)
let insertionPoint = decl.getEnd();
let length = 0;
if (decl.kind == ts.SyntaxKind.Parameter || decl.kind == ts.SyntaxKind.VariableDeclaration) {
const varDecl = <ts.ParameterDeclaration | ts.VariableDeclaration>decl;
if (varDecl.type) {
const start = varDecl.type.getStart();
// console.log(`REPLACING "${varDecl.type.getFullText()}" with "${resolved}"`);
addChange(decl.getSourceFile().fileName, {
span: {
start: start,
length: varDecl.type.getEnd() - start
},
newText: ' ' + resolved
});
} else {
addChange(decl.getSourceFile().fileName, {
span: {
start: varDecl.name.getEnd(),
length: 0
},
newText: ': ' + resolved
});
}
}
}
function typeAndSymbol(n: ts.Node): [ts.Type, ts.Symbol] {
let symbolNode: ts.Node;
switch (n.kind) {
case ts.SyntaxKind.Parameter:
symbolNode = (<ts.ParameterDeclaration>n).name;
break;
case ts.SyntaxKind.VariableDeclaration:
symbolNode = (<ts.VariableDeclaration>n).name;
break;
default:
symbolNode = n;
}
return [checker.getTypeAtLocation(n), checker.getSymbolAtLocation(symbolNode)];
}
function typeToString(t: ts.Type | string | null) {
if (t == null) return null;
if (typeof t === 'string') return t;
if (t.flags & ts.TypeFlags.NumberLiteral) {
return 'number';
}
return checker.typeToString(t);
}
function argsListToString(types: ts.Type[]): string {
return types.map((t, i) => `arg${i + 1}: ${typeToString(t) || 'any'}`).join(', ');
}
function symbolToString(t: ts.Symbol | null) {
return t == null ? null : checker.symbolToString(t);
}
function getConstraint(sym: ts.Symbol): TypeConstraints {
let constraints = allConstraints.get(sym);
if (!constraints) {
const decls = sym.getDeclarations();
let type: ts.Type | undefined;
if (decls.length > 0) {
type = checker.getTypeOfSymbolAtLocation(sym, decls[0]);
}
constraints = new TypeConstraints(services, checker, type && isAny(type) ? undefined : type);
allConstraints.set(sym, constraints);
}
return constraints;
}
/** visit nodes finding exported classes */
function visit(fileName: string, node: ts.Node) {
// console.log(`[${fileName}] Node: ${node.kind}`);
const ctxType = checker.getContextualType(<ts.Expression>node);
if (ctxType) {//} && !isAny(ctxType)) {
const [nodeType, nodeSym] = typeAndSymbol(node);
if (nodeSym) {//&& isAny(nodeType)) {
// const paramType = checker.getTypeOfSymbolAtLocation(param, node);//param.declarations[0]);
// addConstraint(nodeSym, paramType);
// console.log(`CONTEXT TYPE FOR ${symbolToString(nodeSym)} = ${typeToString(ctxType)}`);
getConstraint(nodeSym).isType(ctxType);
}
}
// Property access that is not a method call.
if (node.kind === ts.SyntaxKind.PropertyAccessExpression && !isCallTarget(node)) {
const access = <ts.PropertyAccessExpression>node;
const [targetType, targetSym] = typeAndSymbol(access.expression);
if (targetSym) {//} && isAny(targetType)) {
const fieldConstraints = getConstraint(targetSym).getFieldConstraints(access.name.text);
if (ctxType) {//} && !isAny(ctxType)) {
fieldConstraints.isType(ctxType);
}
}
}
if (node.kind === ts.SyntaxKind.CallExpression) {
const call = <ts.CallExpression>node;
const callee = call.expression;
let callConstraints: CallConstraints | undefined;
if (callee.kind === ts.SyntaxKind.Identifier) {
const [calleeType, calleeSym] = typeAndSymbol(callee);
if (calleeSym) {//} && isAny(calleeType)) {
const argTypes = call.arguments.map(a => checker.getTypeAtLocation(a));
callConstraints = getConstraint(calleeSym).getCallConstraints();
}
} else if (callee.kind === ts.SyntaxKind.PropertyAccessExpression) {
const access = <ts.PropertyAccessExpression>callee;
const [targetType, targetSym] = typeAndSymbol(access.expression);
if (targetSym) {//} && isAny(targetType)) {
callConstraints = getConstraint(targetSym).getFieldConstraints(access.name.text).getCallConstraints();
}
}
if (callConstraints) {
let returnType: ts.Type | undefined = ctxType;
const argTypes = call.arguments.map(a => checker.getTypeAtLocation(a));
console.log(`IS VOID`);
if (returnType) {
callConstraints.returnType.isType(returnType);
} else if (node.parent && node.parent.kind == ts.SyntaxKind.ExpressionStatement) {
// if ((returnType == null || isAny(returnType)) &&
// (node.parent && node.parent.kind == ts.SyntaxKind.ExpressionStatement)) {
callConstraints.returnType.isVoid();
}
argTypes.forEach((t, i) => callConstraints!.getArgType(i).isType(t));
}
}
if (node.kind === ts.SyntaxKind.BinaryExpression) {
const binExpr = <ts.BinaryExpression>node;
const [[leftType, leftSym], [rightType, rightSym]] = [binExpr.left, binExpr.right].map(typeAndSymbol);
// console.log(`[${fileName}] Binary expression: ${node.kind}
// text = "${node.getFullText()}"
// left.type = ${typeToString(leftType)} (symbol = ${symbolToString(leftSym)})
// right.type = ${typeToString(rightType)} (symbol = ${symbolToString(rightSym)})
// `);
if (leftSym || rightType) {
switch (binExpr.operatorToken.kind) {
case ts.SyntaxKind.PlusToken:
if (leftSym) {//} && isAny(leftType)) {
// console.log(`left is any`);
// console.log(`right.flags = ${rightType.flags}`);
if (isNumber(rightType)) {
getConstraint(leftSym).isNumber();
}
}
break;
default:
// console.log(`OP: ${binExpr.operatorToken.kind}`);
}
}
}
ts.forEachChild(node, n => visit(fileName, n));
}
}
function isNumber(t: ts.Type): boolean {
return (t.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral)) !== 0;
}
function isAny(t: ts.Type): boolean {
return (t.flags & ts.TypeFlags.Any) === ts.TypeFlags.Any;
}
function isStructuredType(t: ts.Type): boolean {
return (t.flags & ts.TypeFlags.StructuredType) !== 0;
}
function isCallTarget(n: ts.Node): boolean {
if (n.parent && n.parent.kind == ts.SyntaxKind.CallExpression) {
const call = <ts.CallExpression>n.parent;
return call.expression === n;
}
return false;
}
import * as ts from "typescript";
import {VersionedFile, makeVersionedFile, updateVersionedFile} from './file';
export type AddChangeCallback = (fileName: string, change: ts.TextChange) => void;
export type ReactorCallback = (fileNames: string[], services: ts.LanguageService, addChange: AddChangeCallback) => void;
export class LanguageServiceReactor implements ts.LanguageServiceHost {
private files = new Map<string, VersionedFile>();
private services: ts.LanguageService;
private fileNames: string[];
constructor(
fileContents: Map<string, string>,
private options: ts.CompilerOptions) {
const fileNames: string[] = [];
for (const [fileName, content] of fileContents) {
this.files.set(fileName, makeVersionedFile(content));
fileNames.push(fileName);
}
this.services = ts.createLanguageService(this, ts.createDocumentRegistry());
this.fileNames = fileNames;
}
getScriptFileNames() {
return this.fileNames;
}
getScriptVersion(fileName) {
const file = this.files.get(fileName);
if (file == null) return '';
return file.version.toString();
}
getScriptSnapshot(fileName) {
const file = this.files.get(fileName);
return file && ts.ScriptSnapshot.fromString(file.content);
}
getCurrentDirectory() {
return process.cwd();
}
getCompilationSettings() {
return this.options;
}
getDefaultLibFileName(options) {
return ts.getDefaultLibFilePath(this.options);
}
get fileContents(): Map<string, string> {
const result = new Map<string, string>();
for (const [fileName, file] of this.files) {
result.set(fileName, file.content);
}
return result;
}
react(callback: ReactorCallback): boolean {
const pendingChanges = new Map<string, ts.TextChange[]>();
callback(this.fileNames, this.services, (fileName, change) => {
const changes = pendingChanges.get(fileName);
if (changes) {
changes.push(change);
} else {
pendingChanges.set(fileName, [change]);
}
});
if (pendingChanges.size == 0) {
return false;
}
for (const [fileName, changes] of pendingChanges) {
const file: VersionedFile = this.files.get(fileName)!;
this.files.set(fileName, updateVersionedFile(file, changes));
}
return true;
}
}
/// <reference path="../node_modules/@types/node/index.d.ts" />
import * as fs from "fs";
import * as ts from "typescript";
import {LanguageServiceReactor, AddChangeCallback} from './language_service_reactor';
import {infer} from './infer';
import {format} from './format';
const fileNames = process.argv.slice(2);
const fileContents = new Map<string, string>();
for (const fileName of fileNames) {
fileContents.set(fileName, fs.readFileSync(fileName).toString());
}
const reactor = new LanguageServiceReactor(fileContents, {
allowJs: true,
strictNullChecks: true,
});
const maxIterations = 5;
for (let i = 0; i < maxIterations; i++) {
console.warn(`Running incremental type inference (${i + 1} / ${maxIterations})...`);
if (!reactor.react(infer)) {
break;
}
}
reactor.react(format);
for (const [fileName, content] of reactor.fileContents) {
console.warn(`${fileName}:`);
console.log(content);
}
{
"devDependencies": {
"concurrently": "^3.1.0",
"typescript": "^2.1.5",
"watch": "^1.0.1"
},
"name": "infer-types.ts",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@types/acorn": "^4.0.0",
"@types/estree": "0.0.34",
"@types/node": "^7.0.4",
"acorn": "^3.3.0"
},
"scripts": {
"test:w": "tsc && concurrently \"tsc -w\" \"watch --wait=1 \\\"node build/src/main.js test/data/input.js\\\" build test/data\"",
"test-pipe:w": "tsc && concurrently \"tsc -w\" \"watch --wait=1 \\\"node build/src/main.js test/data/input.js | tee test-output/output.ts\\\" build test/data\""
},
"author": "",
"license": "ISC"
}
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "build",
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"pretty": true,
"removeComments": true,
"sourceMap": true,
"strictNullChecks": true
},
"atom": {
"rewriteTsconfig": true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment