Rewrite using flow
parent
e8db43160f
commit
6c9b19b90d
|
@ -2,8 +2,6 @@
|
|||
.*/node_modules/.*
|
||||
.*/tests/samples/.*
|
||||
.*/lib/.*
|
||||
.*/src/definitions.js
|
||||
.*/src/declarations.js
|
||||
|
||||
[libs]
|
||||
declarations/
|
||||
|
|
|
@ -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
218
src/collector.js
218
src/collector.js
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
52
src/list.js
52
src/list.js
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
12
src/scope.js
12
src/scope.js
|
@ -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]);
|
||||
|
|
65
src/utils.js
65
src/utils.js
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue