Use flow to check code

master
Paul Loyd 2017-11-18 12:38:34 +03:00
parent 29ec78a994
commit 94535791e9
20 changed files with 2294 additions and 272 deletions

View File

@ -1,5 +1,6 @@
{
"presets": [
"@babel/flow",
["@babel/env", {
"targets": {
"node": "6.10"

17
.flowconfig Normal file
View File

@ -0,0 +1,17 @@
[ignore]
.*/node_modules/.*
.*/tests/samples/.*
.*/src/extractors.js
[libs]
declarations/
[lints]
all=warn
[options]
all=true
module.use_strict=true
munge_underscores=true
include_warnings=true
unsafe.enable_getters_and_setters=true

1754
declarations/babel.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
declare module 'json-stringify-pretty-compact' {
declare export default (any, ?{maxLength?: number}) => string;
}

3
declarations/mocha.js Normal file
View File

@ -0,0 +1,3 @@
declare function describe(string, Function): void;
declare function it(string, Function): void;
declare function before(Function): void;

11
declarations/optimist.js Normal file
View File

@ -0,0 +1,11 @@
declare module 'optimist' {
declare type Optimist = {
usage(string): Optimist,
argv: {
[string]: any,
_: string[],
},
};
declare module.exports: Optimist;
}

3
declarations/resolve.js Normal file
View File

@ -0,0 +1,3 @@
declare module 'resolve' {
declare function sync(string, ?{basedir: string}): string;
}

View File

@ -23,6 +23,7 @@
"flow2schema": "./bin/flow2schema"
},
"dependencies": {
"@babel/types": "^7.0.0-beta.32",
"babylon": "^7.0.0-beta.32",
"json-stringify-pretty-compact": "^1.0.4",
"optimist": "^0.6.1",
@ -32,7 +33,9 @@
"@babel/cli": "^7.0.0-beta.32",
"@babel/core": "^7.0.0-beta.32",
"@babel/preset-env": "^7.0.0-beta.32",
"@babel/preset-flow": "^7.0.0-beta.32",
"@babel/register": "^7.0.0-beta.32",
"flow-bin": "^0.59.0",
"jasmine": "^2.8.0",
"mocha": "^4.0.1",
"nyc": "^11.3.0"
@ -40,6 +43,7 @@
"scripts": {
"prepare": "npm run build",
"build": "babel src/ -d lib/",
"test": "nyc mocha -r @babel/register -R list tests/run.js"
"test": "flow && nyc mocha -r @babel/register -R list tests/run.js",
"flow": "flow"
}
}

View File

@ -9,7 +9,7 @@ const argv = optimist
argv._.forEach(run);
function run(path) {
function run(path: string) {
if (path === '-') {
path = '/dev/stdin';
}

View File

@ -1,34 +1,60 @@
import * as assert from 'assert';
import * as fs from 'fs';
import * as pathlib from 'path';
import type {Node} from '@babel/types';
import globals from './globals';
// $FlowFixMe
import * as extractors from './extractors';
import Command from './commands';
import Module from './module';
import Scope from './scope';
import CircularList from './list';
import {isNode} from './utils';
import {invariant, isNode} 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>,
};
type InstanceParam = {
name: string,
value: Schema,
};
export default class Collector {
constructor(parser, root = '.') {
+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.tasks = new CircularList;
this.taskCount = 0;
this.active = true;
this.modules = new Map;
this.roots = new Set;
this.global = Scope.global(globals);
this.running = false;
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, internal = false) {
collect(path: string, internal: boolean = false) {
// TODO: follow symlinks.
path = pathlib.resolve(path);
let module = this.modules.get(path);
let module = this._modules.get(path);
if (module) {
return;
@ -43,18 +69,18 @@ export default class Collector {
module = new Module(path, namespace);
const scope = this.global.extend(module);
const scope = this._global.extend(module);
this._freestyle(extractors.declaration, ast.program, scope, null);
this._freestyle(extractors.declaration, ast.program, scope, []);
this.modules.set(path, module);
this._modules.set(path, module);
if (this.running) {
if (this._running) {
return;
}
try {
this.running = true;
this._running = true;
this._schedule();
if (!internal) {
@ -63,12 +89,12 @@ export default class Collector {
this._schedule();
}
} finally {
this.running = false;
this._running = false;
}
}
// Given the AST output of babylon parse, walk through in a depth-first order.
_freestyle(group, root, scope, params) {
_freestyle(group: Group, root: Node, scope: Scope, params: InstanceParam[]) {
let stack;
let parent;
let keys = [];
@ -86,12 +112,13 @@ export default class Collector {
continue;
}
// $FlowFixMe
const node = parent ? parent[keys[index]] : root;
if (isNode(node) && isAcceptableGroup(group, node)) {
if (!this.roots.has(node)) {
if (!this._roots.has(node)) {
const task = this._collect(group, node, scope, params);
this.roots.add(node);
this._roots.add(node);
this._spawn(task);
}
@ -107,11 +134,12 @@ export default class Collector {
} while (stack);
}
* _collect(group, node, scope, params) {
* _collect(group: Group, node: Node, scope: Scope, params: InstanceParam[]): Task {
// $FlowFixMe
const extractor = group[node.type];
if (!extractor) {
this._freestyle(group, node, scope, null);
this._freestyle(group, node, scope, []);
return null;
}
@ -120,7 +148,7 @@ export default class Collector {
let result = null;
while (true) {
this.active = true;
this._active = true;
const {done, value} = iter.next(result);
@ -128,83 +156,78 @@ export default class Collector {
return value;
}
assert.ok(value);
invariant(value);
if (value instanceof Command) {
switch (value.name) {
case 'declare':
scope.addDeclaration(...value.data);
break;
case 'define':
const [schema, declared] = value.data;
if (declared && params) {
const name = schema.name;
schema.name = generateGenericName(name, params);
scope.addInstance(name, schema, params.map(p => p.value));
} else {
scope.addDefinition(...value.data);
}
this.schemas.push(schema);
break;
case 'external':
scope.addImport(value.data);
break;
case 'provide':
scope.addExport(...value.data);
break;
case 'query':
if (params) {
const param = params.find(p => p.name === value.data[0]);
if (param) {
result = param.value;
break;
}
}
result = yield* this._query(scope, ...value.data);
break;
case 'enter':
scope = scope.extend();
break;
case 'exit':
assert.ok(scope.parent);
scope = scope.parent;
break;
case 'namespace':
result = scope.namespace;
break;
}
if (isNode(value)) {
result = yield* this._collect(group, value, scope, params);
} else if (Array.isArray(value)) {
result = [];
for (const val of value) {
result.push(yield* this._collect(group, val, scope, params));
}
} else {
assert.ok(isNode(value));
result = yield* this._collect(group, value, 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, name, params) {
* _query(scope: Scope, name: string, params: Schema[]): Task {
let result = scope.query(name, params);
// TODO: warning.
assert.notEqual(result.type, 'unknown');
invariant(result.type !== 'unknown');
// Resulting scope is always the best choice for waiting.
scope = result.scope;
@ -218,7 +241,10 @@ export default class Collector {
this.collect(modulePath, true);
const module = this.modules.get(modulePath);
const module = this._modules.get(modulePath);
invariant(module);
const {imported} = result.info;
while ((result = module.query(imported, params)).type === 'unknown') {
@ -230,7 +256,7 @@ export default class Collector {
}
// TODO: reexports.
assert.ok(result.type === 'declaration' || result.type === 'template');
invariant(result.type === 'declaration' || result.type === 'template');
scope = result.scope;
name = result.name;
@ -238,23 +264,27 @@ export default class Collector {
// Fallthrough.
case 'declaration':
case 'template':
let tmplParams = null;
const tmplParams = [];
if (result.type === 'template') {
tmplParams = result.params.map((p, i) => ({
name: p.name,
value: params[i] || p.default,
}));
for (const [i, p] of result.params.entries()) {
tmplParams.push({
name: p.name,
value: params[i] || p.default,
});
}
}
invariant(result.type === 'declaration' || result.type === 'template');
this._freestyle(extractors.definition, result.node, scope, tmplParams);
while ((result = scope.query(name, params)).type !== 'definition') {
assert.notEqual(result.type, 'external');
invariant(result.type !== 'external');
yield;
}
assert.equal(result.type, 'definition');
invariant(result.type === 'definition');
// Fallthrough.
case 'definition':
@ -262,19 +292,19 @@ export default class Collector {
}
}
* _grabExports(module) {
* _grabExports(module: Module): Task {
for (const [scope, name] of module.exports()) {
yield* this._query(scope, name, null);
yield* this._query(scope, name, []);
}
}
_spawn(task) {
this.tasks.add(task);
_spawn(task: Task) {
this._tasks.add(task);
++this.taskCount;
}
_schedule() {
const {tasks} = this;
const tasks = this._tasks;
let marker = null;
@ -290,9 +320,9 @@ export default class Collector {
tasks.add(task);
if (this.active) {
if (this._active) {
marker = task;
this.active = false;
this._active = false;
} else if (task === marker) {
// TODO: warning.
return;
@ -301,7 +331,7 @@ export default class Collector {
}
}
function pathToNamespace(path) {
function pathToNamespace(path: string): string {
const pathObj = pathlib.parse(path);
return pathlib.format({
@ -313,15 +343,16 @@ function pathToNamespace(path) {
.join('.');
}
function isAcceptableGroup(group, node) {
function isAcceptableGroup(group: Group, node: Node): boolean {
// $FlowFixMe
return group.entries.includes(node.type);
}
function generateGenericName(base, params) {
function generateGenericName(base: string, params: InstanceParam[]): string {
let name = base + '_';
for (const {value} of params) {
assert.equal(typeof value, 'string');
invariant(typeof value === 'string');
name += '_' + value;
}

View File

@ -1,38 +1,66 @@
export default class Command {
constructor(name, data) {
this.name = name;
this.data = data;
}
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 declare(name, node, params) {
return new Command('declare', [name, node, params]);
export function define(schema: Schema, declared: boolean = true): Command {
return {
kind: 'define',
schema,
declared,
};
}
export function define(schema, declared = true) {
return new Command('define', [schema, declared]);
export function external(external: ExternalInfo): Command {
return {
kind: 'external',
external,
};
}
export function external(external) {
return new Command('external', external);
export function provide(name: string, reference: string = name): Command {
return {
kind: 'provide',
name,
reference,
};
}
export function provide(name, reference = name) {
return new Command('provide', [name, reference]);
export function query(name: string, params: ?Schema[]): Command {
return {
kind: 'query',
name,
params: params || [],
};
}
export function query(name, params = null) {
return new Command('query', [name, params]);
export function enter(): Command {
return {kind: 'enter'};
}
export function enter() {
return new Command('enter');
export function exit(): Command {
return {kind: 'exit'};
}
export function exit() {
return new Command('exit');
}
export function namespace() {
return new Command('namespace');
export function namespace(): Command {
return {kind: 'namespace'};
}

View File

@ -1,7 +1,10 @@
import * as assert from 'assert';
import * as t from '@babel/types';
import {declare, define, external, provide, query, enter, exit, namespace} from './commands';
import {partition, isNode} from './utils';
import {invariant, partition} from './utils';
import type {Schema} from './schema';
type E = Generator<any, any, any>;
export const definition = {
entries: [
@ -10,7 +13,7 @@ export const definition = {
'ClassDeclaration',
],
* TypeAlias(node) {
* TypeAlias(node: t.TypeAlias): E {
let schema = yield node.right;
if (typeof schema === 'string') {
@ -25,7 +28,7 @@ export const definition = {
return schema;
},
* InterfaceDeclaration(node) {
* InterfaceDeclaration(node: t.InterfaceDeclaration): E {
let schema = yield node.body;
if (node.extends.length > 0) {
@ -51,7 +54,7 @@ export const definition = {
return schema;
},
* ClassDeclaration(node) {
* ClassDeclaration(node: t.ClassDeclaration): E {
let schema = yield node.body;
if (node.superClass) {
@ -69,14 +72,14 @@ export const definition = {
return schema;
},
* ClassBody(node) {
* ClassBody(node: t.ClassBody): E {
return {
type: 'record',
fields: (yield node.body).filter(Boolean),
};
},
* ClassProperty(node) {
* ClassProperty(node: t.ClassProperty): E {
if (node.static) {
return null;
}
@ -84,17 +87,17 @@ export const definition = {
return yield* extractProperty(node, node.typeAnnotation);
},
* ClassMethod(node) {
* ClassMethod(node: t.ClassMethod): E {
return null;
},
* ObjectTypeAnnotation(node) {
* ObjectTypeAnnotation(node: t.ObjectTypeAnnotation): E {
if (node.indexers.length > 0) {
// Allow functions, getters and setters.
const properties = (yield node.properties).filter(Boolean);
assert.equal(properties.length, 0);
assert.equal(node.indexers.length, 1);
invariant(properties.length === 0);
invariant(node.indexers.length === 1);
return {
type: 'map',
@ -108,42 +111,42 @@ export const definition = {
};
},
* ObjectTypeProperty(node) {
* ObjectTypeProperty(node: t.ObjectTypeProperty): E {
return yield* extractProperty(node, node.value);
},
* ObjectTypeIndexer(node) {
* ObjectTypeIndexer(node: t.ObjectTypeIndexer): E {
const key = yield node.key;
assert.equal(key, 'string');
invariant(key === 'string');
return yield node.value;
},
* TypeAnnotation(node) {
* TypeAnnotation(node: t.TypeAnnotation): E {
return yield node.typeAnnotation;
},
* NumberTypeAnnotation(node) {
* NumberTypeAnnotation(node: t.NumberTypeAnnotation): E {
return 'double';
},
* StringTypeAnnotation(node) {
* StringTypeAnnotation(node: t.StringTypeAnnotation): E {
return 'string';
},
* BooleanTypeAnnotation(node) {
* BooleanTypeAnnotation(node: t.BooleanTypeAnnotation): E {
return 'boolean';
},
* ArrayTypeAnnotation(node) {
* ArrayTypeAnnotation(node: t.ArrayTypeAnnotation): E {
return {
type: 'array',
items: yield node.elementType,
};
},
* UnionTypeAnnotation(node) {
* UnionTypeAnnotation(node: t.UnionTypeAnnotation): E {
// TODO: flatten variants.
let [symbols, variants] = partition(node.types, isEnumSymbol);
@ -167,10 +170,11 @@ export const definition = {
return variants;
},
* IntersectionTypeAnnotation(node) {
* 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);
@ -180,22 +184,22 @@ export const definition = {
return mergeSchemas(schemas);
},
* NullableTypeAnnotation(node) {
* NullableTypeAnnotation(node: t.NullableTypeAnnotation): E {
return ['null', yield node.typeAnnotation];
},
* NullLiteralTypeAnnotation(node) {
* NullLiteralTypeAnnotation(node: t.NullLiteralTypeAnnotation): E {
return 'null';
},
* StringLiteralTypeAnnotation(node) {
* StringLiteralTypeAnnotation(node: t.StringLiteralTypeAnnotation): E {
return {
type: 'enum',
symbols: [node.value],
};
},
* GenericTypeAnnotation(node) {
* GenericTypeAnnotation(node: t.GenericTypeAnnotation): E {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
@ -218,27 +222,27 @@ export const definition = {
return makeFullname(schema);
},
* TypeParameterInstantiation(node) {
* TypeParameterInstantiation(node: t.TypeParameterInstantiation): E {
return yield node.params;
},
* FunctionTypeAnnotation(node) {
* FunctionTypeAnnotation(node: t.FunctionTypeAnnotation): E {
return null;
},
* InterfaceExtends(node) {
* InterfaceExtends(node: t.InterfaceExtends): E {
return yield node.id;
},
* Identifier(node) {
* Identifier(node: t.Identifier): E {
return node.name;
},
* CommentLine(node) {
* CommentLine(node: t.CommentLine): E {
return extractPragma(node.value);
},
* CommentBlock(node) {
* CommentBlock(node: t.CommentBlock): E {
return extractPragma(node.value);
},
};
@ -260,11 +264,11 @@ export const declaration = {
* Blocks.
*/
* Program(node) {
* Program(node: t.Program): E {
yield node.body;
},
* BlockStatement(node) {
* BlockStatement(node: t.BlockStatement): E {
yield enter();
yield node.body;
yield exit();
@ -277,7 +281,7 @@ export const declaration = {
* TODO: support form "import *".
*/
* ImportDeclaration(node) {
* ImportDeclaration(node: t.ImportDeclaration): E {
const specifiers = yield node.specifiers;
const path = yield node.source;
@ -288,21 +292,21 @@ export const declaration = {
}
},
* ImportDefaultSpecifier(node) {
* ImportDefaultSpecifier(node: t.ImportDefaultSpecifier): E {
return {
local: yield node.local,
imported: null,
};
},
* ImportSpecifier(node) {
* ImportSpecifier(node: t.ImportSpecifier): E {
return {
local: yield node.local,
imported: yield node.imported,
};
},
* VariableDeclarator(node) {
* VariableDeclarator(node: t.VariableDeclarator): E {
const path = extractRequire(node.init);
if (!path) {
@ -325,11 +329,11 @@ export const declaration = {
}
},
* ObjectPattern(node) {
* ObjectPattern(node: t.ObjectPattern): E {
return yield node.properties;
},
* ObjectProperty(node) {
* ObjectProperty(node: t.ObjectProperty): E {
const key = yield node.key;
// TODO: different roots.
@ -337,7 +341,7 @@ export const declaration = {
return null;
}
//assert.equal(node.value.type, 'Identifier');
//invariant(node.value.type === 'Identifier');
const value = yield node.value;
@ -354,7 +358,7 @@ export const declaration = {
* TODO: support commonjs.
*/
* ExportDefaultDeclaration(node) {
* ExportDefaultDeclaration(node: t.ExportDefaultDeclaration): E {
const reference = yield node.declaration;
if (reference) {
@ -362,7 +366,7 @@ export const declaration = {
}
},
* ExportNamedDeclaration(node) {
* ExportNamedDeclaration(node: t.ExportNamedDeclaration): E {
if (!node.declaration) {
yield node.specifiers;
return;
@ -375,7 +379,7 @@ export const declaration = {
}
},
* ExportSpecifier(node) {
* ExportSpecifier(node: t.ExportSpecifier): E {
const reference = yield node.local;
let name = yield node.exported;
@ -390,7 +394,7 @@ export const declaration = {
* Declarations.
*/
* TypeAlias(node) {
* TypeAlias(node: t.TypeAlias): E {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
@ -399,7 +403,7 @@ export const declaration = {
return name;
},
* InterfaceDeclaration(node) {
* InterfaceDeclaration(node: t.InterfaceDeclaration): E {
const name = yield node.id;
const params = node.typeParameters && (yield node.typeParameters);
@ -408,24 +412,24 @@ export const declaration = {
return name;
},
* ClassDeclaration(node) {
* 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(is('ClassMethod'));
yield body.body.filter(n => t.isClassMethod(n));
yield declare(name, node, params);
return name;
},
* TypeParameterDeclaration(node) {
* TypeParameterDeclaration(node: t.TypeParameterDeclaration): E {
return yield node.params;
},
* TypeParameter(node) {
* TypeParameter(node: t.TypeParameter): E {
return {
name: node.name,
default: node.default ? yield node.default : null,
@ -436,16 +440,16 @@ export const declaration = {
* Utility.
*/
* StringLiteral(node) {
* StringLiteral(node: t.StringLiteral): E {
return node.value;
},
* Identifier(node) {
* Identifier(node: t.Identifier): E {
return node.name;
},
};
function* extractLastPragma(comments) {
function* extractLastPragma(comments: t.Comment[]): ?string {
const pragmas = (yield comments).filter(Boolean);
return pragmas.length > 0 ? pragmas[pragmas.length - 1] : null;
@ -481,6 +485,7 @@ function* extractProperty(prop, value) {
function extractRequire(node) {
// XXX: refactor it!
// TODO: use `t.*` helpers.
const ok = node &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
@ -493,7 +498,7 @@ function extractRequire(node) {
const argument = node.arguments[0];
// TODO: warning about dynamic imports.
assert.equal(argument.type, 'StringLiteral');
invariant(t.isStringLiteral(argument));
return argument.value;
}
@ -531,7 +536,7 @@ function extractPragma(text) {
const pair = parsePragma(pragma);
assert.ok(pair);
invariant(pair);
const [type, arg] = pair;
@ -570,12 +575,12 @@ function unwrapEnumSymbol(node) {
}
function makeFullname(schema) {
assert.ok(schema.namespace);
invariant(schema.namespace);
return `${schema.namespace}.${schema.name}`;
}
function mergeSchemas(schemas) {
function mergeSchemas(schemas: Schemas): Schema {
const map = new Map;
// TODO: overriding?
@ -584,7 +589,7 @@ function mergeSchemas(schemas) {
for (const schema of schemas) {
// TODO: enums?
assert.equal(schema.type, 'record');
invariant(schema.type === 'record');
for (const field of schema.fields) {
const stored = map.get(field.name);
@ -592,7 +597,7 @@ function mergeSchemas(schemas) {
if (stored) {
// TODO: what about enums?
// TODO: improve checking.
assert.equal(stored.type, field.type);
invariant(stored.type === field.type);
continue;
}
@ -608,7 +613,3 @@ function mergeSchemas(schemas) {
fields: Array.from(map.values()),
};
}
function is(type) {
return node => Boolean(node) && node.type === type;
}

View File

@ -4,7 +4,7 @@ import Collector from './collector';
// @see babel#6805.
//export {Parser, Collector};
export default function collect(path) {
export default function collect(path: string): Collector {
const parser = new Parser;
const collector = new Collector(parser);

View File

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

View File

@ -1,23 +1,32 @@
import * as pathlib from 'path';
import * as resolve from 'resolve';
import type Scope from './scope';
import type {Schema} from './schema';
import type {Query} from './query';
export default class Module {
constructor(path, namespace) {
+path: string;
+namespace: string;
_scopeCount: number;
_exports: Map<?string, [Scope, string]>;
constructor(path: string, namespace: string) {
this.path = path;
this.namespace = namespace;
this.scopeCount = 0;
this._scopeCount = 0;
this._exports = new Map;
}
generateScopeId() {
return this.scopeCount++;
generateScopeId(): number {
return this._scopeCount++;
}
addExport(name, scope, reference) {
addExport(name: ?string, scope: Scope, reference: string) {
this._exports.set(name, [scope, reference]);
}
query(name, params) {
query(name: ?string, params: Schema[]): Query {
const result = this._exports.get(name);
if (!result) {
@ -31,14 +40,14 @@ export default class Module {
return scope.query(reference, params);
}
resolve(path) {
resolve(path: string): string {
const basedir = pathlib.dirname(this.path);
// TODO: follow symlinks.
return resolve.sync(path, {basedir});
}
exports() {
exports(): Iterator<[Scope, string]> {
return this._exports.values();
}
}

View File

@ -1,7 +1,8 @@
import * as babylon from 'babylon';
import type {File} from '@babel/types';
export default class Parser {
parse(code) {
parse(code: string): File {
// This parse configuration is intended to be as permissive as possible.
return babylon.parse(code, {
allowImportExportEverywhere: true,

59
src/query.js Normal file
View File

@ -0,0 +1,59 @@
import type {Node} from '@babel/types';
import type Scope from './scope';
import type {Schema} from './schema';
export type Query =
| Unknown
| Declaration
| Template
| Definition
| External;
export type Unknown = {
type: 'unknown',
};
export type Declaration = {
type: 'declaration',
name: string,
node: Node,
scope: Scope,
};
export type Template = {
type: 'template',
name: string,
params: TemplateParam[],
instances: Instance[],
node: Node,
scope: Scope,
};
export type Definition = {
type: 'definition',
schema: Schema,
scope: Scope,
};
export type External = {
type: 'external',
info: ExternalInfo,
scope: Scope,
};
export type TemplateParam = {
name: string,
default: Schema,
};
export type Instance = {
params: Schema[],
schema: Schema,
};
export type ExternalInfo = {
local: string,
imported: ?string,
path: string,
};

77
src/schema.js Normal file
View File

@ -0,0 +1,77 @@
// @see flow#3912.
export type Schema =
| RecordType & Top
| EnumType & Top
| ArrayType & Top
| MapType & Top
| UnionType & Top
| FixedType & Top
| WrappedType & Top;
export type Top = {
name: string,
namespace?: string,
$unwrap?: boolean,
};
export type Type =
| ComplexType
| PrimitiveType
| ReferenceType;
export type WrappedType = {type: Type};
export type ComplexType =
| RecordType
| EnumType
| ArrayType
| MapType
| UnionType
| FixedType
| WrappedType;
export type PrimitiveType =
| 'null'
| 'boolean'
| 'int'
| 'long'
| 'float'
| 'double'
| 'bytes'
| 'string';
export type ReferenceType = string;
export type RecordType = {
type: 'record',
name: string,
fields: FieldType[],
};
export type FieldType = {
name: string,
type: Type,
};
export type EnumType = {
type: 'enum',
name: string,
symbols: string[],
};
export type ArrayType = {
type: 'array',
items: Type,
};
export type MapType = {
type: 'map',
values: Type,
};
export type UnionType = Type[];
export type FixedType = {
type: 'fixed',
size: number,
};

View File

@ -1,7 +1,17 @@
import * as assert from 'assert';
import type {Node} from '@babel/types';
import {invariant} from './utils';
import type Module from './module';
import type {Schema} from './schema';
import type {Query, Template, TemplateParam, ExternalInfo} from './query';
export default class Scope {
static global(schemas) {
+parent: ?Scope;
+module: ?Module;
+scopeId: ?number;
_entries: Map<string, Query>;
static global(schemas: Schema[]) {
const global = new Scope(null, null);
for (const schema of schemas) {
@ -11,104 +21,102 @@ export default class Scope {
return global;
}
constructor(parent, module) {
constructor(parent: ?Scope, module: ?Module) {
this.parent = parent;
this.module = module;
this.scopeId = module && module.generateScopeId();
this.entries = new Map;
this._entries = new Map;
}
get namespace() {
assert.ok(this.module);
get namespace(): string {
invariant(this.module);
let namespace = this.module.namespace;
// Nested scopes.
if (this.scopeId > 0) {
if (this.scopeId != null && this.scopeId > 0) {
namespace += '._' + this.scopeId;
}
return namespace;
}
extend(module = null) {
extend(module: ?Module = null): Scope {
return new Scope(this, module || this.module);
}
addDeclaration(name, node, params) {
assert.ok(!this.entries.has(name));
addDeclaration(name: string, node: Node, params: TemplateParam[]) {
invariant(!this._entries.has(name));
const isTemplate = Boolean(params);
const entry = {
type: isTemplate ? 'template' : 'declaration',
const entry = params.length > 0 ? {
type: 'template',
name,
params,
instances: [],
node,
scope: this,
} : {
type: 'declaration',
name,
node,
scope: this,
};
if (isTemplate) {
entry.params = params;
entry.instances = [];
}
this.entries.set(name, entry);
this._entries.set(name, entry);
}
addInstance(name, schema, params) {
const template = this.entries.get(name);
addInstance(name: string, schema: Schema, params: Schema[]) {
const template = this._entries.get(name);
assert.ok(template);
assert.equal(template.type, 'template');
invariant(template);
invariant(template.type === 'template');
template.instances.push({params, schema});
}
addDefinition(schema, declared) {
const decl = this.entries.get(schema.name);
addDefinition(schema: Schema, declared: boolean) {
const decl = this._entries.get(schema.name);
if (declared) {
assert.ok(decl);
assert.equal(decl.type, 'declaration');
invariant(decl);
invariant(decl.type === 'declaration');
} else {
assert.ok(!decl);
invariant(!decl);
}
this.entries.set(schema.name, {
this._entries.set(schema.name, {
type: 'definition',
schema,
scope: this,
});
}
addImport(info) {
assert.ok(!this.entries.has(info.local));
addImport(info: ExternalInfo) {
invariant(!this._entries.has(info.local));
this.entries.set(info.local, {
this._entries.set(info.local, {
type: 'external',
info,
scope: this,
});
}
addExport(name, reference) {
assert.ok(this.module);
addExport(name: string, reference: string) {
invariant(this.module);
this.module.addExport(name, this, reference);
}
resolve(path) {
assert.ok(this.module);
resolve(path: string): string {
invariant(this.module);
return this.module.resolve(path);
}
query(name, params) {
const entry = this.entries.get(name);
query(name: string, params: Schema[]): Query {
const entry = this._entries.get(name);
if (entry && entry.type === 'template') {
assert.ok(params);
const augmented = entry.params.map((p, i) => params[i] || p.default);
const schema = findInstance(entry, augmented);
@ -126,7 +134,7 @@ export default class Scope {
}
if (this.parent) {
return this.parent.query(name);
return this.parent.query(name, params);
}
return {
@ -135,7 +143,7 @@ export default class Scope {
}
}
function findInstance(template, queried) {
function findInstance(template: Template, queried: Schema[]): ?Schema {
for (const {schema, params} of template.instances) {
// TODO: compare complex structures.
const same = params.every((p, i) => p === queried[i]);

View File

@ -1,4 +1,6 @@
export function partition(iter, predicate) {
import * as assert from 'assert';
export function partition<T>(iter: Iterable<T>, predicate: T => boolean): [T[], T[]] {
const left = [];
const right = [];
@ -9,6 +11,11 @@ export function partition(iter, predicate) {
return [left, right];
}
export function isNode(it) {
// TODO: avoid it?
export function isNode(it: any): boolean %checks {
return it && typeof it === 'object' && it.type;
}
// I so much dream about the user guards...
// @see flow#112.
export const invariant = assert.ok;