Rewrite using flow

master
Paul Loyd 2017-11-21 15:28:59 +03:00
parent e8db43160f
commit 6c9b19b90d
17 changed files with 1348 additions and 1428 deletions

View File

@ -2,8 +2,6 @@
.*/node_modules/.*
.*/tests/samples/.*
.*/lib/.*
.*/src/definitions.js
.*/src/declarations.js
[libs]
declarations/

View File

@ -45,5 +45,6 @@ Output (`$ ./bin/flow2schema example.js`):
## TODO
* Complete generics support.
* Errors and warnings.
* Support "declare".
* Support commonjs modules.
* Documentation.

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +1,39 @@
import * as fs from 'fs';
import * as pathlib from 'path';
import {isNode} from '@babel/types';
import type {Node} from '@babel/types';
import traverse from './traverse';
import globals from './globals';
// $FlowFixMe
import definitionGroup from './definitions';
// $FlowFixMe
import declarationGroup from './declarations';
import Module from './module';
import Scope from './scope';
import CircularList from './list';
import {invariant, isNode} from './utils';
import Context from './context';
import {invariant, get, map} from './utils';
import type Parser from './parser';
import type {Schema} from './schema';
type Task = Generator<void, ?Schema, void>;
type Group = {
entries: string[],
[string]: Node => Generator<any, any, any>,
};
import type {Schema, Type} from './schema';
type InstanceParam = {
name: string,
value: Schema,
value: ?Type,
};
const VISITOR = Object.assign({}, definitionGroup, declarationGroup);
export default class Collector {
+root: string;
+parser: Parser;
+schemas: Schema[];
taskCount: number;
_tasks: CircularList<Task>;
_active: boolean;
_modules: Map<string, Module>;
_roots: Set<Node>;
_global: Scope;
_running: boolean;
constructor(parser: Parser, root: string = '.') {
this.root = root;
this.parser = parser;
this.schemas = [];
this.taskCount = 0;
this._tasks = new CircularList;
this._active = true;
this._modules = new Map;
this._roots = new Set;
this._global = Scope.global(globals);
this._running = false;
}
collect(path: string, internal: boolean = false) {
@ -74,135 +57,34 @@ export default class Collector {
const scope = this._global.extend(module);
this._freestyle(declarationGroup, ast.program, scope, []);
this._freestyle(ast.program, scope, []);
this._modules.set(path, module);
if (this._running) {
return;
}
try {
this._running = true;
this._schedule();
if (!internal) {
const task = this._grabExports(module);
this._spawn(task);
this._schedule();
}
} finally {
this._running = false;
if (!internal) {
this._grabExports(module);
}
}
_freestyle(group: Group, root: Node, scope: Scope, params: InstanceParam[]) {
_freestyle(root: Node, scope: Scope, params: InstanceParam[]) {
const ctx = new Context(this, scope, params);
const iter = traverse(root);
let result = iter.next();
while (!result.done) {
const node = result.value;
const detain = isAcceptableGroup(group, node);
const detain = node.type in VISITOR;
if (detain && !this._roots.has(node)) {
const task = this._collect(group, node, scope, params);
this._roots.add(node);
this._spawn(task);
if (detain) {
VISITOR[node.type](ctx, node);
}
result = iter.next(detain);
}
}
* _collect(group: Group, node: Node, scope: Scope, params: InstanceParam[]): Task {
const extractor = group[node.type];
if (!extractor) {
this._freestyle(group, node, scope, []);
return null;
}
const iter = extractor(node);
let result = null;
while (true) {
this._active = true;
const {done, value} = iter.next(result);
if (done) {
return value;
}
invariant(value);
if (isNode(value)) {
result = yield* this._collect(group, value, scope, params);
} else if (value instanceof Array) {
result = [];
for (const val of value) {
result.push(yield* this._collect(group, val, scope, params));
}
} else switch (value.kind) {
case 'declare':
scope.addDeclaration(value.name, value.node, value.params);
break;
case 'define':
const {schema, declared} = value;
if (declared && params.length > 0) {
const name = schema.name;
schema.name = generateGenericName(name, params);
scope.addInstance(name, schema, params.map(p => p.value));
} else {
scope.addDefinition(schema, declared);
}
this.schemas.push(schema);
break;
case 'external':
scope.addImport(value.external);
break;
case 'provide':
scope.addExport(value.name, value.reference);
break;
case 'query':
const param = params.find(p => p.name === value.name);
if (param) {
// TODO: warning about missing param.
result = param.value;
} else {
result = yield* this._query(scope, value.name, value.params);
}
break;
case 'enter':
scope = scope.extend();
break;
case 'exit':
invariant(scope.parent);
scope = scope.parent;
break;
case 'namespace':
result = scope.namespace;
break;
}
}
}
* _query(scope: Scope, name: string, params: Schema[]): Task {
_query(scope: Scope, name: string, params: (?Type)[]): Type {
let result = scope.query(name, params);
// TODO: warning.
@ -226,9 +108,7 @@ export default class Collector {
const {imported} = result.info;
while ((result = module.query(imported, params)).type === 'unknown') {
yield;
}
result = module.query(imported, params);
if (result.type === 'definition') {
return result.schema;
@ -249,19 +129,16 @@ export default class Collector {
for (const [i, p] of result.params.entries()) {
tmplParams.push({
name: p.name,
value: params[i] || p.default,
value: params[i] === undefined ? p.default : params[i],
});
}
}
invariant(result.type === 'declaration' || result.type === 'template');
this._freestyle(definitionGroup, result.node, scope, tmplParams);
this._freestyle(result.node, scope, tmplParams);
while ((result = scope.query(name, params)).type !== 'definition') {
invariant(result.type !== 'external');
yield;
}
result = scope.query(name, params);
invariant(result.type === 'definition');
@ -269,43 +146,13 @@ export default class Collector {
case 'definition':
return result.schema;
}
invariant(false);
}
* _grabExports(module: Module): Task {
_grabExports(module: Module) {
for (const [scope, name] of module.exports()) {
yield* this._query(scope, name, []);
}
}
_spawn(task: Task) {
this._tasks.add(task);
++this.taskCount;
}
_schedule() {
const tasks = this._tasks;
let marker = null;
while (!tasks.isEmpty) {
const task = tasks.remove();
const {done} = task.next();
if (done) {
marker = null;
continue;
}
tasks.add(task);
if (this._active) {
marker = task;
this._active = false;
} else if (task === marker) {
// TODO: warning.
return;
}
this._query(scope, name, []);
}
}
}
@ -321,18 +168,3 @@ function pathToNamespace(path: string): string {
.split(pathlib.sep)
.join('.');
}
function isAcceptableGroup(group: Group, node: Node): boolean {
return group.entries.includes(node.type);
}
function generateGenericName(base: string, params: InstanceParam[]): string {
let name = base + '_';
for (const {value} of params) {
invariant(typeof value === 'string');
name += '_' + value;
}
return name;
}

View File

@ -1,66 +0,0 @@
import type {Node} from '@babel/types';
import type {Schema} from './schema';
import type {TemplateParam, ExternalInfo} from './query';
export type Command =
| {kind: 'declare', name: string, node: Node, params: TemplateParam[]}
| {kind: 'define', schema: Schema, declared: boolean}
| {kind: 'external', external: ExternalInfo}
| {kind: 'provide', name: string, reference: string}
| {kind: 'query', name: string, params: Schema[]}
| {kind: 'enter'}
| {kind: 'exit'}
| {kind: 'namespace'};
export function declare(name: string, node: Node, params: ?TemplateParam[]): Command {
return {
kind: 'declare',
name,
node,
params: params || [],
};
}
export function define(schema: Schema, declared: boolean = true): Command {
return {
kind: 'define',
schema,
declared,
};
}
export function external(external: ExternalInfo): Command {
return {
kind: 'external',
external,
};
}
export function provide(name: string, reference: string = name): Command {
return {
kind: 'provide',
name,
reference,
};
}
export function query(name: string, params: ?Schema[]): Command {
return {
kind: 'query',
name,
params: params || [],
};
}
export function enter(): Command {
return {kind: 'enter'};
}
export function exit(): Command {
return {kind: 'exit'};
}
export function namespace(): Command {
return {kind: 'namespace'};
}

99
src/context.js Normal file
View File

@ -0,0 +1,99 @@
import type {Node} from '@babel/types';
import Scope from './scope';
import Collector from './collector';
import {invariant, get, map} from './utils';
import type {Schema, Type, ComplexType} from './schema';
import type {TemplateParam, ExternalInfo} from './query';
type InstanceParam = {
name: string,
value: ?Type,
};
export default class Context {
_collector: Collector;
_scope: Scope;
_params: InstanceParam[];
constructor(collector: Collector, scope: Scope, params: InstanceParam[]) {
this._collector = collector;
this._scope = scope;
this._params = params;
}
get namespace(): string {
return this._scope.namespace;
}
freestyle(node: Node) {
this._collector._freestyle(node, this._scope, this._params);
}
declare(name: string, node: Node, params: ?TemplateParam[]) {
this._scope.addDeclaration(name, node, params || []);
}
define(name: string, type: ComplexType, declared: boolean = true): Schema {
// TODO: dirty support for intersections.
const _name = type.name != null ? type.name : name;
const schema: $FlowFixMe = Object.assign({}, type, {
name,
namespace: this._scope.namespace,
});
if (declared && this._params.length > 0) {
schema.name = generateGenericName(name, this._params);
this._scope.addInstance(name, schema, map(this._params, get('value')));
} else {
this._scope.addDefinition(schema, declared);
}
this._collector.schemas.push(schema);
return schema;
}
external(external: ExternalInfo) {
this._scope.addImport(external);
}
provide(name: ?string, reference: string) {
this._scope.addExport(name, reference);
}
query(name: string, params: ?(?Type)[]): Type {
const param = this._params.find(p => p.name === name);
if (param) {
// TODO: warning about missing param.
invariant(param.value != null);
return param.value;
}
return this._collector._query(this._scope, name, params || []);
}
enter() {
this._scope = this._scope.extend();
}
exit() {
invariant(this._scope.parent);
this._scope = this._scope.parent;
}
}
function generateGenericName(base: string, params: InstanceParam[]): string {
let name = base + '_';
for (const {value} of params) {
invariant(typeof value === 'string');
name += '_' + value;
}
return name;
}

View File

@ -1,229 +1,226 @@
import * as t from '@babel/types';
// flow#5376.
import type {
Block, ClassDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, Identifier,
ImportDeclaration, ImportDefaultSpecifier, ImportSpecifier, InterfaceDeclaration,
Node, TypeAlias, TypeParameterDeclaration, VariableDeclaration, VariableDeclarator,
} from '@babel/types';
import {declare, external, provide, enter, exit} from './commands';
import {invariant} from './utils';
import {
isCallExpression, isClassDeclaration, isClassMethod, isExportDefaultDeclaration,
isExportNamedDeclaration, isIdentifier, isImportDeclaration, isImportNamespaceSpecifier,
isImportSpecifier, isInterfaceDeclaration, isObjectPattern, isObjectProperty, isProgram,
isStringLiteral, isTypeAlias, isVariableDeclaration,
} from '@babel/types';
type E = Generator<any, any, any>;
import {invariant, map, filter} from './utils';
import Context from './context';
import type {ExternalInfo, TemplateParam} from './query';
export default {
entries: [
// Blocks.
'Program',
'BlockStatement',
// Imports.
'ImportDeclaration',
'VariableDeclarator',
// Exports.
'ExportNamedDeclaration',
'ExportDefaultDeclaration',
],
/*
* Blocks.
*/
/*
* Blocks.
*/
function processBlock(ctx: Context, node: Block) {
const scoped = isProgram(node);
* Program(node: t.Program): E {
yield node.body;
},
if (!scoped) {
ctx.enter();
}
* BlockStatement(node: t.BlockStatement): E {
yield enter();
yield node.body;
yield exit();
},
for (const entry of node.body) {
processBodyEntry(ctx, entry);
}
/*
* Imports.
*
* TODO: warning about "import typeof".
* TODO: support form "import *".
*/
if (!scoped) {
ctx.exit();
}
}
* ImportDeclaration(node: t.ImportDeclaration): E {
const specifiers = yield node.specifiers;
const path = yield node.source;
function processBodyEntry(ctx: Context, node: Node) {
if (isDeclaration(node)) {
processDeclaration(ctx, node);
return;
}
for (const specifier of specifiers) {
specifier.path = path;
if (isImportDeclaration(node)) {
processImportDeclaration(ctx, node);
return;
}
yield external(specifier);
}
},
if (isExportNamedDeclaration(node)) {
processExportNamedDeclaration(ctx, node);
return;
}
* ImportDefaultSpecifier(node: t.ImportDefaultSpecifier): E {
return {
local: yield node.local,
imported: null,
};
},
if (isExportDefaultDeclaration(node)) {
processExportDefaultDeclaration(ctx, node);
return;
}
* ImportSpecifier(node: t.ImportSpecifier): E {
return {
local: yield node.local,
imported: yield node.imported,
};
},
if (isVariableDeclaration(node)) {
processVariableDeclaration(ctx, node);
return;
}
* VariableDeclarator(node: t.VariableDeclarator): E {
const path = extractRequire(node.init);
// TODO: do it only for "all"-mode.
ctx.freestyle(node);
}
if (!path) {
return null;
/*
* Imports.
*
* TODO: warning about "import typeof".
* TODO: support form "import *".
* TODO: support complex patterns.
*/
function processImportDeclaration(ctx: Context, node: ImportDeclaration) {
const path = node.source.value;
for (const specifier of node.specifiers) {
if (isImportNamespaceSpecifier(specifier)) {
continue;
}
let specifiers = yield node.id;
ctx.external(extractExternal(specifier, path));
}
}
if (typeof specifiers === 'string') {
specifiers = [{
local: specifiers,
imported: null,
}];
}
function extractExternal(node: ImportSpecifier | ImportDefaultSpecifier, path: string): ExternalInfo {
return {
local: node.local.name,
imported: isImportSpecifier(node) ? node.imported.name : null,
path,
};
}
for (const specifier of specifiers) {
specifier.path = path;
function processVariableDeclaration(ctx: Context, node: VariableDeclaration) {
for (const declarator of node.declarations) {
processVariableDeclarator(ctx, declarator);
}
}
yield external(specifier);
}
},
function processVariableDeclarator(ctx: Context, node: VariableDeclarator) {
const path = extractRequire(node.init);
* ObjectPattern(node: t.ObjectPattern): E {
return yield node.properties;
},
* ObjectProperty(node: t.ObjectProperty): E {
const key = yield node.key;
// TODO: different roots.
if (node.value.type !== 'Identifier') {
return null;
}
//invariant(node.value.type === 'Identifier');
const value = yield node.value;
return {
local: value,
imported: key,
};
},
/*
* Exports.
*
* TODO: support "export from" form.
* TODO: support commonjs.
*/
* ExportDefaultDeclaration(node: t.ExportDefaultDeclaration): E {
const reference = yield node.declaration;
if (reference) {
yield provide(null, reference);
}
},
* ExportNamedDeclaration(node: t.ExportNamedDeclaration): E {
if (!node.declaration) {
yield node.specifiers;
return;
}
const reference = yield node.declaration;
if (reference) {
yield provide(reference);
}
},
* ExportSpecifier(node: t.ExportSpecifier): E {
const reference = yield node.local;
let name = yield node.exported;
if (name === 'default') {
name = null;
}
yield provide(name, reference);
},
/*
* Declarations.
*/
* TypeAlias(node: t.TypeAlias): E {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
yield declare(name, node, params);
return name;
},
* InterfaceDeclaration(node: t.InterfaceDeclaration): E {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
yield declare(name, node, params);
return name;
},
* ClassDeclaration(node: t.ClassDeclaration): E {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
// TODO: do it only for "all"-mode.
const body = node.body;
yield body.body.filter(n => t.isClassMethod(n));
yield declare(name, node, params);
return name;
},
* TypeParameterDeclaration(node: t.TypeParameterDeclaration): E {
return yield node.params;
},
* TypeParameter(node: t.TypeParameter): E {
return {
name: node.name,
default: node.default ? yield node.default : null,
};
},
/*
* Utility.
*/
* StringLiteral(node: t.StringLiteral): E {
return node.value;
},
* Identifier(node: t.Identifier): E {
return node.name;
},
};
function extractRequire(node) {
// XXX: refactor it!
// TODO: use `t.*` helpers.
const ok = node &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require';
if (!ok) {
if (path == null) {
return null;
}
const argument = node.arguments[0];
const {id} = node;
// TODO: warning about dynamic imports.
invariant(t.isStringLiteral(argument));
if (isIdentifier(id)) {
const external = extractCommonjsDefaultExternal(id, path);
return argument.value;
ctx.external(external);
} else if (isObjectPattern(id)) {
const externals = extractCommonjsNamedExternals(id.properties, path);
externals.forEach(external => ctx.external(external));
}
}
function extractRequire(node: Node): ?string {
return isCallExpression(node)
&& isIdentifier(node.callee, {name: 'require'})
&& node.arguments.length > 0
// TODO: warning about dynamic imports.
&& isStringLiteral(node.arguments[0])
? node.arguments[0].value : null;
}
function extractCommonjsDefaultExternal(node: Identifier, path: string): ExternalInfo {
return {
local: node.name,
imported: null,
path,
};
}
function extractCommonjsNamedExternals<+T: Node>(nodes: T[], path: string): ExternalInfo[] {
const pred = (n): %checks => isObjectProperty(n) && isIdentifier(n.key) && isIdentifier(n.value);
const make = prop => ({
local: (prop: $FlowFixMe).value.name,
imported: (prop: $FlowFixMe).key.name,
path,
});
return map(filter(nodes, pred), make);
}
/*
* Exports.
*
* TODO: support "export from" form.
* TODO: support commonjs.
*/
function processExportNamedDeclaration(ctx: Context, node: ExportNamedDeclaration) {
if (isDeclaration(node.declaration)) {
const reference = processDeclaration(ctx, node.declaration);
ctx.provide(reference, reference);
}
for (const specifier of node.specifiers) {
const reference = specifier.local.name;
const exported = specifier.exported.name;
const name = exported === 'default' ? null : exported;
ctx.provide(name, reference);
}
}
function processExportDefaultDeclaration(ctx: Context, node: ExportDefaultDeclaration) {
if (!isDeclaration(node.declaration)) {
return;
}
const reference = processDeclaration(ctx, node.declaration);
ctx.provide(null, reference);
}
/*
* Declarations.
*
* TODO: support defaults in generics.
* TODO: support "declare ..." form.
*/
type Declaration = TypeAlias | InterfaceDeclaration | ClassDeclaration;
function isDeclaration(node: mixed): boolean %checks {
return isTypeAlias(node) || isInterfaceDeclaration(node) || isClassDeclaration(node);
}
function processDeclaration(ctx: Context, node: Declaration) {
const {name} = node.id;
const params = node.typeParameters && extractTemplateParams(node.typeParameters);
// TODO: do it only for "all"-mode.
if (isClassDeclaration(node)) {
const methods = filter(node.body.body, isClassMethod);
for (const method of methods) {
ctx.freestyle(method);
}
}
ctx.declare(name, node, params);
return name;
}
function extractTemplateParams(node: TypeParameterDeclaration): TemplateParam[] {
return map(node.params, param => ({
name: param.name,
default: null,
}));
}
export default {
Program: processBlock,
BlockStatement: processBlock,
};

View File

@ -1,306 +1,329 @@
import * as t from '@babel/types';
// flow#5376.
import type {
ArrayTypeAnnotation, ClassDeclaration, ClassProperty, Comment, FlowTypeAnnotation,
GenericTypeAnnotation, InterfaceDeclaration, IntersectionTypeAnnotation, TypeAlias,
UnionTypeAnnotation, NullableTypeAnnotation, ObjectTypeIndexer, ObjectTypeProperty,
StringLiteralTypeAnnotation,
} from '@babel/types';
import {define, query, namespace} from './commands';
import {invariant, partition} from './utils';
import type {Schema} from './schema';
import {
isIdentifier, isObjectTypeProperty, isStringLiteralTypeAnnotation, isClassProperty,
} from '@babel/types';
type E = Generator<any, any, any>;
import Context from './context';
export default {
entries: [
'TypeAlias',
'InterfaceDeclaration',
'ClassDeclaration',
],
import type {
Schema, Type, ReferenceType, ComplexType, RecordType, FixedType,
FieldType, ArrayType, MapType, UnionType, EnumType,
} from './schema';
* TypeAlias(node: t.TypeAlias): E {
let schema = yield node.right;
import {isPrimitiveType, isComplexType, makeFullname, mergeTypes} from './schema';
import {invariant, compose, last, get, negate, partition, map, filter, filterMap, compact} from './utils';
if (typeof schema === 'string') {
schema = {type: schema};
}
function processTypeAlias(ctx: Context, node: TypeAlias) {
let type = makeType(ctx, node.right);
schema.name = yield node.id;
schema.namespace = yield namespace();
// TODO: support function aliases.
invariant(type != null);
// TODO: support top-level unions.
invariant(!(type instanceof Array));
yield define(schema);
if (typeof type === 'string') {
type = {type: type};
}
return schema;
},
* InterfaceDeclaration(node: t.InterfaceDeclaration): E {
let schema = yield node.body;
if (node.extends.length > 0) {
const schemas = [];
for (const extend of node.extends) {
const name = yield extend;
const schema = yield query(name);
schemas.push(schema);
}
schemas.push(schema);
schema = mergeSchemas(schemas);
}
schema.name = yield node.id;
schema.namespace = yield namespace();
yield define(schema);
return schema;
},
* ClassDeclaration(node: t.ClassDeclaration): E {
let schema = yield node.body;
if (node.superClass) {
const name = yield node.superClass;
const superSchema = yield query(name);
schema = mergeSchemas([superSchema, schema]);
}
schema.name = yield node.id;
schema.namespace = yield namespace();
yield define(schema);
return schema;
},
* ClassBody(node: t.ClassBody): E {
return {
type: 'record',
fields: (yield node.body).filter(Boolean),
};
},
* ClassProperty(node: t.ClassProperty): E {
if (node.static) {
return null;
}
return yield* extractProperty(node, node.typeAnnotation);
},
* ClassMethod(node: t.ClassMethod): E {
return null;
},
* ObjectTypeAnnotation(node: t.ObjectTypeAnnotation): E {
if (node.indexers.length > 0) {
// Allow functions, getters and setters.
const properties = (yield node.properties).filter(Boolean);
invariant(properties.length === 0);
invariant(node.indexers.length === 1);
return {
type: 'map',
values: yield node.indexers[0],
};
}
return {
type: 'record',
fields: (yield node.properties).filter(Boolean),
};
},
* ObjectTypeProperty(node: t.ObjectTypeProperty): E {
return yield* extractProperty(node, node.value);
},
* ObjectTypeIndexer(node: t.ObjectTypeIndexer): E {
const key = yield node.key;
invariant(key === 'string');
return yield node.value;
},
* TypeAnnotation(node: t.TypeAnnotation): E {
return yield node.typeAnnotation;
},
* NumberTypeAnnotation(node: t.NumberTypeAnnotation): E {
return 'double';
},
* StringTypeAnnotation(node: t.StringTypeAnnotation): E {
return 'string';
},
* BooleanTypeAnnotation(node: t.BooleanTypeAnnotation): E {
return 'boolean';
},
* ArrayTypeAnnotation(node: t.ArrayTypeAnnotation): E {
return {
type: 'array',
items: yield node.elementType,
};
},
* UnionTypeAnnotation(node: t.UnionTypeAnnotation): E {
// TODO: flatten variants.
let [symbols, variants] = partition(node.types, isEnumSymbol);
symbols = symbols.map(unwrapEnumSymbol);
variants = yield variants;
if (symbols.length > 0) {
const enumeration = {
type: 'enum',
symbols,
};
if (variants.length === 0) {
return enumeration;
}
variants.push(enumeration);
}
return variants;
},
* IntersectionTypeAnnotation(node: t.IntersectionTypeAnnotation): E {
const schemas = [];
for (const type of node.types) {
// TODO: support arbitrary types, not only references.
const name = yield type;
const schema = yield query(name);
schemas.push(schema);
}
return mergeSchemas(schemas);
},
* NullableTypeAnnotation(node: t.NullableTypeAnnotation): E {
return ['null', yield node.typeAnnotation];
},
* NullLiteralTypeAnnotation(node: t.NullLiteralTypeAnnotation): E {
return 'null';
},
* StringLiteralTypeAnnotation(node: t.StringLiteralTypeAnnotation): E {
return {
type: 'enum',
symbols: [node.value],
};
},
* GenericTypeAnnotation(node: t.GenericTypeAnnotation): E {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
const schema = yield query(name, params);
if (typeof schema === 'string') {
return schema;
}
if (schema.$unwrap) {
return schema.type;
}
const enclosing = yield namespace();
if (schema.namespace === enclosing) {
return schema.name;
}
return makeFullname(schema);
},
* TypeParameterInstantiation(node: t.TypeParameterInstantiation): E {
return yield node.params;
},
* FunctionTypeAnnotation(node: t.FunctionTypeAnnotation): E {
return null;
},
* InterfaceExtends(node: t.InterfaceExtends): E {
return yield node.id;
},
* Identifier(node: t.Identifier): E {
return node.name;
},
* CommentLine(node: t.CommentLine): E {
return extractPragma(node.value);
},
* CommentBlock(node: t.CommentBlock): E {
return extractPragma(node.value);
},
};
function* extractLastPragma(comments: t.Comment[]): ?string {
const pragmas = (yield comments).filter(Boolean);
return pragmas.length > 0 ? pragmas[pragmas.length - 1] : null;
ctx.define(node.id.name, type);
}
function* extractProperty(prop, value) {
let type = null;
// TODO: type params.
function processInterfaceDeclaration(ctx: Context, node: InterfaceDeclaration) {
let type = makeType(ctx, node.body);
if (prop.leadingComments) {
type = yield* extractLastPragma(prop.leadingComments);
invariant(type != null)
invariant(isComplexType(type));
if (node.extends.length > 0) {
const types = [];
for (const extend of node.extends) {
const name = extend.id.name;
const type = ctx.query(name);
invariant(isComplexType(type));
types.push((type: $FlowFixMe));
}
types.push((type: $FlowFixMe));
[, type] = mergeTypes(types);
}
if (!type) {
type = yield value;
ctx.define(node.id.name, type);
}
// TODO: type params.
function processClassDeclaration(ctx: Context, node: ClassDeclaration) {
const props: $FlowFixMe = filter(node.body.body, isClassProperty);
let type = makeRecord(ctx, props);
if (node.superClass) {
// TODO: warning about expressions here.
invariant(isIdentifier(node.superClass));
const {name} = node.superClass;
const superSchema = ctx.query(name);
invariant(isComplexType(superSchema));
[, type] = mergeTypes([(superSchema: $FlowFixMe), (type: $FlowFixMe)]);
}
if (!type) {
ctx.define(node.id.name, type);
}
function makeType(ctx: Context, node: FlowTypeAnnotation): ?Type {
switch (node.type) {
case 'NullLiteralTypeAnnotation':
return 'null';
case 'BooleanTypeAnnotation':
return 'boolean';
case 'NumberTypeAnnotation':
return 'double';
case 'StringTypeAnnotation':
return 'string';
case 'TypeAnnotation':
return makeType(ctx, node.typeAnnotation);
case 'NullableTypeAnnotation':
return makeNullable(ctx, node);
case 'ObjectTypeAnnotation':
const map = makeMap(ctx, node.indexers);
const record = makeRecord(ctx, node.properties);
// TODO: warning about this.
invariant(!map || record.fields.length === 0);
return map || record;
case 'ArrayTypeAnnotation':
return makeArrayType(ctx, node);
case 'UnionTypeAnnotation':
return makeUnionType(ctx, node);
case 'IntersectionTypeAnnotation':
return makeIntersection(ctx, node);
case 'StringLiteralTypeAnnotation':
return makeEnum(node);
case 'GenericTypeAnnotation':
return makeReference(ctx, node);
case 'FunctionTypeAnnotation':
return null;
default:
invariant(false, `Unknown type: ${node.type}`);
}
}
function makeNullable(ctx: Context, node: NullableTypeAnnotation): ?UnionType {
const type = makeType(ctx, node.typeAnnotation);
if (type == null) {
return null;
}
if (type.type === 'record') {
type.namespace = yield namespace();
yield define(type, false);
type = type.name;
}
return ['null', type];
}
function makeRecord<T: ObjectTypeProperty | ClassProperty>(ctx: Context, nodes: T[]): RecordType {
const fields = compact(map(nodes, node => makeField(ctx, node)));
return {
name: yield prop.key,
type: 'record',
fields,
};
}
function makeField(ctx: Context, node: ObjectTypeProperty | ClassProperty): ?FieldType {
// $FlowFixMe
if (node.static) {
return null;
}
let type = null;
if (node.leadingComments) {
const pragmas = extractPragmas(node.leadingComments);
type = last(pragmas);
}
if (type == null) {
const value = isObjectTypeProperty(node) ? node.value : node.typeAnnotation;
// TODO: no type annotation for the class property.
invariant(value);
type = makeType(ctx, value);
}
if (type == null) {
return null;
}
if (isComplexType(type) && type.type === 'record') {
const name = (type: $FlowFixMe).name;
ctx.define(name, type, false);
type = name;
}
// TODO: support optional fields.
// TODO: warning about computed properties.
invariant(isObjectTypeProperty(node) || !node.computed);
invariant(isIdentifier(node.key));
return {
name: node.key.name,
type,
};
}
function parsePragma(pragma) {
let [type, arg] = pragma.split(/\s+/);
if (isPrimitive(type)) {
if (arg != null) {
return null;
}
} else if (type === 'fixed') {
arg = Number(arg);
if (!Number.isInteger(arg)) {
return null;
}
} else {
function makeMap(ctx: Context, nodes: ObjectTypeIndexer[]): ?MapType {
if (nodes.length === 0) {
return null;
}
return [type, arg];
// TODO: what to do in this case?
invariant(nodes.length === 1);
const node = nodes[0];
invariant(makeType(ctx, node.key) === 'string');
const values = makeType(ctx, node.value);
if (values == null) {
return null;
}
return {
type: 'map',
values,
};
}
function extractPragma(text) {
function makeArrayType(ctx: Context, node: ArrayTypeAnnotation): ?ArrayType {
const items = makeType(ctx, node.elementType);
if (items == null) {
return null;
}
return {
type: 'array',
items,
};
}
function makeUnionType(ctx: Context, node: UnionTypeAnnotation): ?(UnionType | EnumType) {
// TODO: flatten variants.
// TODO: refactor it.
let [symbols, variants] = partition(node.types, isStringLiteralTypeAnnotation);
// $FlowFixMe
symbols = map(symbols, get('value'));
variants = compact(map(variants, node => makeType(ctx, node)));
if (symbols.length > 0) {
const enumeration: EnumType = {
type: 'enum',
symbols,
};
if (variants.length === 0) {
return enumeration;
}
variants.push(enumeration);
}
if (variants.length === 0) {
return null;
}
return variants;
}
function makeIntersection(ctx: Context, node: IntersectionTypeAnnotation): ?Type {
const types = [];
// TODO: refactor it.
for (const typeNode of node.types) {
const type = makeType(ctx, typeNode);
if (type == null) {
continue;
}
// TODO: support arbitrary types, not only references.
invariant(typeof type === 'string');
const queried = ctx.query(type);
invariant(isComplexType(queried));
types.push((queried: $FlowFixMe));
}
if (types.length === 0) {
return null;
}
const [name, intersection] = mergeTypes(types);
// TODO: dirty support for intersections.
(intersection: $FlowFixMe).name = name;
return intersection;
}
function makeEnum(node: StringLiteralTypeAnnotation): EnumType {
return {
type: 'enum',
symbols: [node.value],
};
}
function makeReference(ctx: Context, node: GenericTypeAnnotation): ReferenceType {
const {name} = node.id;
const params = node.typeParameters && map(node.typeParameters.params, n => makeType(ctx, n));
const type = ctx.query(name, params);
if (typeof type === 'string') {
return type;
}
// TODO: generalized it.
if ((type: $FlowFixMe).$unwrap) {
invariant(typeof type.type === 'string');
return type.type;
}
invariant(isComplexType(type));
if (type.namespace === ctx.namespace) {
return (type: $FlowFixMe).name;
}
return makeFullname((type: $FlowFixMe));
}
function extractPragmas(comments: Comment[]): Type[] {
return filterMap(comments, compose(get('value'), extractPragma));
}
function extractPragma(text: string): ?Type {
const marker = '$avro ';
const value = text.trimLeft();
@ -311,82 +334,34 @@ function extractPragma(text) {
const pragma = value.slice(marker.length).trim();
const pair = parsePragma(pragma);
return parsePragma(pragma);
}
invariant(pair);
function parsePragma(pragma: string): Type {
let [type, arg] = pragma.split(/\s+/);
const [type, arg] = pair;
if (isPrimitiveType(type)) {
invariant(arg == null);
return type;
}
if (type === 'fixed') {
return {
arg = Number(arg);
invariant(Number.isInteger(arg));
return ({
type: 'fixed',
size: arg,
};
}: FixedType);
}
return type;
invariant(false);
}
function isEnumSymbol(node) {
return node.type === 'StringLiteralTypeAnnotation';
}
function isPrimitive(type) {
switch (type) {
case 'null':
case 'int':
case 'long':
case 'float':
case 'double':
case 'bytes':
case 'string':
case 'boolean':
return true;
default:
return false;
}
}
function unwrapEnumSymbol(node) {
return node.value;
}
function makeFullname(schema) {
invariant(schema.namespace);
return `${schema.namespace}.${schema.name}`;
}
function mergeSchemas(schemas: Schema[]): Schema {
const map = new Map;
// TODO: overriding?
// TODO: anonymous?
let name = '';
for (const schema of schemas) {
// TODO: enums?
invariant(schema.type === 'record');
for (const field of schema.fields) {
const stored = map.get(field.name);
if (stored) {
// TODO: what about enums?
// TODO: improve checking.
invariant(stored.type === field.type);
continue;
}
map.set(field.name, field);
}
name += '_' + schema.name;
}
return {
type: 'record',
name,
fields: Array.from(map.values()),
};
export default {
TypeAlias: processTypeAlias,
InterfaceDeclaration: processInterfaceDeclaration,
ClassDeclaration: processClassDeclaration,
}

View File

@ -1,52 +0,0 @@
import {invariant} from './utils';
export default class CircularList<T: Object> {
_mark: Symbol;
_prev: ?T;
_walk: ?T;
constructor() {
this._mark = Symbol();
this._prev = null;
this._walk = null;
}
get isEmpty(): boolean {
return !this._walk;
}
add(entry: T) {
invariant(!entry[this._mark]);
if (this._prev) {
invariant(this._walk);
this._prev = this._prev[this._mark] = entry;
} else {
invariant(!this._walk);
this._walk = this._prev = entry;
}
entry[this._mark] = this._walk;
invariant(!this._prev || this._prev[this._mark] === this._walk);
}
remove(): T {
invariant(this._walk);
const removed = this._walk;
if (removed === this._prev) {
this._walk = this._prev = null;
} else {
invariant(this._prev);
this._walk = this._prev[this._mark] = removed[this._mark];
}
removed[this._mark] = null;
return removed;
}
}

View File

@ -2,7 +2,7 @@ import * as pathlib from 'path';
import * as resolve from 'resolve';
import type Scope from './scope';
import type {Schema} from './schema';
import type {Schema, Type} from './schema';
import type {Query} from './query';
export default class Module {
@ -26,7 +26,7 @@ export default class Module {
this._exports.set(name, [scope, reference]);
}
query(name: ?string, params: Schema[]): Query {
query(name: ?string, params: (?Type)[]): Query {
const result = this._exports.get(name);
if (!result) {

View File

@ -1,7 +1,7 @@
import type {Node} from '@babel/types';
import type Scope from './scope';
import type {Schema} from './schema';
import type {Schema, Type} from './schema';
export type Query =
| Unknown
@ -44,11 +44,11 @@ export type External = {
export type TemplateParam = {
name: string,
default: Schema,
default: ?Type,
};
export type Instance = {
params: Schema[],
params: (?Type)[],
schema: Schema,
};

View File

@ -1,10 +1,13 @@
import {invariant} from './utils';
// @see flow#3912.
export type Schema =
| RecordType & Top
| EnumType & Top
| ArrayType & Top
| MapType & Top
| UnionType & Top
// TODO: support top-level unions.
//| UnionType & Top
| FixedType & Top
| WrappedType & Top;
@ -16,6 +19,7 @@ export type Top = {
export type Type =
| ComplexType
| UnionType
| PrimitiveType
| ReferenceType;
@ -26,7 +30,8 @@ export type ComplexType =
| EnumType
| ArrayType
| MapType
| UnionType
// TODO: unions should be complex types.
//| UnionType & Top
| FixedType
| WrappedType;
@ -44,7 +49,6 @@ export type ReferenceType = string;
export type RecordType = {
type: 'record',
name: string,
fields: FieldType[],
};
@ -55,7 +59,6 @@ export type FieldType = {
export type EnumType = {
type: 'enum',
name: string,
symbols: string[],
};
@ -75,3 +78,70 @@ export type FixedType = {
type: 'fixed',
size: number,
};
export function isPrimitiveType(type: Type): boolean %checks {
// Switch operator is not allowed in %checks functions.
return type === 'null'
|| type === 'int'
|| type === 'long'
|| type === 'float'
|| type === 'double'
|| type === 'bytes'
|| type === 'string'
|| type === 'boolean';
}
export function isComplexType(type: Type): boolean %checks {
return typeof type !== 'string' && !(type instanceof Array);
}
export function makeFullname(schema: Top): string {
invariant(schema.namespace != null);
return `${schema.namespace}.${schema.name}`;
}
export function mergeTypes<+T: ComplexType & {+name?: string}>(types: T[]): [string, ComplexType] {
invariant(types.length > 1);
if (types.length === 1) {
const type = types[0];
// TODO: anonymous?
invariant(type.name != null);
return [type.name, (type: $FlowFixMe)];
}
const map = new Map;
// TODO: overriding?
let name = '';
for (const type of types) {
// TODO: enums?
invariant(type.type === 'record');
for (const field of (type: $FlowFixMe).fields) {
const stored = map.get(field.name);
if (stored) {
// TODO: what about enums?
// TODO: improve checking.
invariant(stored.type === field.type);
continue;
}
map.set(field.name, field);
}
// TODO: anonymous?
name += '_' + (type.name != null ? type.name : 'unnamed');
}
const type = {
type: 'record',
fields: Array.from(map.values()),
};
return [name, (type: $FlowFixMe)];
}

View File

@ -2,7 +2,7 @@ import type {Node} from '@babel/types';
import {invariant} from './utils';
import type Module from './module';
import type {Schema} from './schema';
import type {Schema, Type} from './schema';
import type {Query, Template, TemplateParam, ExternalInfo} from './query';
export default class Scope {
@ -65,7 +65,7 @@ export default class Scope {
this._entries.set(name, entry);
}
addInstance(name: string, schema: Schema, params: Schema[]) {
addInstance(name: string, schema: Schema, params: (?Type)[]) {
const template = this._entries.get(name);
invariant(template);
@ -101,7 +101,7 @@ export default class Scope {
});
}
addExport(name: string, reference: string) {
addExport(name: ?string, reference: string) {
invariant(this.module);
this.module.addExport(name, this, reference);
@ -113,11 +113,11 @@ export default class Scope {
return this.module.resolve(path);
}
query(name: string, params: Schema[]): Query {
query(name: string, params: (?Type)[]): Query {
const entry = this._entries.get(name);
if (entry && entry.type === 'template') {
const augmented = entry.params.map((p, i) => params[i] || p.default);
const augmented = entry.params.map((p, i) => params[i] === undefined ? p.default : params[i]);
const schema = findInstance(entry, augmented);
if (schema) {
@ -143,7 +143,7 @@ export default class Scope {
}
}
function findInstance(template: Template, queried: Schema[]): ?Schema {
function findInstance(template: Template, queried: (?Type)[]): ?Schema {
for (const {schema, params} of template.instances) {
// TODO: compare complex structures.
const same = params.every((p, i) => p === queried[i]);

View File

@ -1,5 +1,9 @@
import * as assert from 'assert';
// I so much dream about the user guards...
// @see flow#112.
export const invariant = assert.ok;
export function partition<T>(iter: Iterable<T>, predicate: T => boolean): [T[], T[]] {
const left = [];
const right = [];
@ -11,11 +15,60 @@ export function partition<T>(iter: Iterable<T>, predicate: T => boolean): [T[],
return [left, right];
}
// TODO: avoid it?
export function isNode(it: mixed): boolean %checks {
return it != null && typeof it === 'object' && it.type != null;
export function map<T, R>(iter: Iterable<T>, mapper: T => R): R[] {
const result = [];
for (const item of iter) {
result.push(mapper(item));
}
return result;
}
// I so much dream about the user guards...
// @see flow#112.
export const invariant = assert.ok;
export function filter<T>(iter: Iterable<T>, predicate: T => boolean): T[] {
const result = [];
for (const item of iter) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
export function filterMap<T, R>(iter: Iterable<T>, mapper: T => ?R): R[] {
const result = [];
for (const item of iter) {
const it = mapper(item);
if (it != null) {
result.push(it);
}
}
return result;
}
export function compact<T>(iter: Iterable<?T>): T[] {
// $FlowFixMe
return filter(iter, Boolean);
}
// $FlowFixMe
export function get<T: Object, K: $Keys<T>>(key: K): T => $ElementType<T, K> {
return obj => obj[key];
}
export function negate<T>(pred: T => boolean): T => boolean {
return flag => !flag;
}
export function compose<X, Y, Z>(a: X => Y, b: Y => Z): X => Z {
return x => b(a(x));
}
export function last<T>(list: T[]): T | void {
return list.length > 0 ? list[list.length - 1] : undefined;
}

View File

@ -16,10 +16,6 @@ function run(title) {
it('should provide expected schemas', () => {
assert.deepEqual(actual.schemas, expected.schemas);
});
it('should run expected tasks', () => {
assert.equal(actual.taskCount, expected.taskCount);
});
}
function main() {

View File

@ -1,27 +1,5 @@
{
"schemas": [
{
"type": "string",
"name": "X",
"namespace": "scopes"
},
{
"type": "record",
"name": "Y",
"namespace": "scopes",
"fields": [{"name": "x", "type": "X"}]
},
{
"type": "double",
"name": "X",
"namespace": "scopes._1"
},
{
"type": "record",
"name": "Y",
"namespace": "scopes._1",
"fields": [{"name": "x", "type": "X"}]
},
{
"type": "boolean",
"name": "X",
@ -68,6 +46,28 @@
{"name": "x", "type": "X"},
{"name": "z", "type": "scopes._1.Z"}
]
},
{
"type": "double",
"name": "X",
"namespace": "scopes._1"
},
{
"type": "record",
"name": "Y",
"namespace": "scopes._1",
"fields": [{"name": "x", "type": "X"}]
},
{
"type": "string",
"name": "X",
"namespace": "scopes"
},
{
"type": "record",
"name": "Y",
"namespace": "scopes",
"fields": [{"name": "x", "type": "X"}]
}
],
"taskCount": 17

View File

@ -1,13 +1,5 @@
{
"schemas": [
{
"type": "record",
"name": "X",
"namespace": "shadowing",
"fields": [
{"name": "x", "type": "bytes"}
]
},
{
"type": "record",
"name": "Buffer",
@ -21,6 +13,14 @@
"fields": [
{"name": "y", "type": "Buffer"}
]
},
{
"type": "record",
"name": "X",
"namespace": "shadowing",
"fields": [
{"name": "x", "type": "bytes"}
]
}
],
"taskCount": 6