Make the definition process lazy

master
Paul Loyd 2017-11-09 12:10:21 +03:00
parent a72929ea17
commit dbd1927aab
39 changed files with 336 additions and 211 deletions

View File

@ -19,11 +19,12 @@ class Collector {
this.taskCount = 0;
this.active = true;
this.modules = new Map;
this.roots = new Set;
this.global = Scope.global(globals);
this.running = false;
}
collect(path) {
collect(path, internal = false) {
// TODO: follow symlinks.
path = pathlib.resolve(path);
@ -44,22 +45,30 @@ class Collector {
const scope = this.global.extend(module);
this._spawn(ast.program, scope);
this._freestyle(extractors.declaration, ast.program, scope);
this.modules.set(path, module);
if (!this.running) {
try {
this.running = true;
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;
}
} finally {
this.running = false;
}
}
// Given the AST output of babylon parse, walk through in a depth-first order.
_spawn(root, scope) {
_freestyle(group, root, scope) {
let stack;
let parent;
let keys = [];
@ -79,20 +88,17 @@ class Collector {
const node = parent ? parent[keys[index]] : root;
if (isNode(node)) {
const {type} = node;
const group = type && getExtractorsGroup(type);
if (group) {
if (isNode(node) && isAcceptableGroup(group, node)) {
if (!this.roots.has(node)) {
const task = this._collect(group, node, scope);
this.tasks.add(task);
++this.taskCount;
continue;
this.roots.add(node);
this._spawn(task);
}
continue;
}
if (isNode(node) || node instanceof Array) {
stack = { parent, keys, index, prev: stack };
parent = node;
keys = Object.keys(node);
@ -105,7 +111,7 @@ class Collector {
const extractor = group[node.type];
if (!extractor) {
assert.fail(`No extractor for "${node.type}" in group "${group.entries[0]}"`);
this._freestyle(group, node, scope);
return null;
}
@ -126,13 +132,13 @@ class Collector {
if (value instanceof Command) {
switch (value.name) {
case 'spawn':
this._spawn(value.data, scope);
case 'declare':
scope.addDeclaration(value.data[0], value.data[1]);
break;
case 'define':
scope.addSchema(value.data);
this.schemas.push(value.data);
scope.addDefinition(value.data[0], value.data[1]);
this.schemas.push(value.data[0]);
break;
case 'external':
@ -140,7 +146,7 @@ class Collector {
break;
case 'provide':
scope.addExport(value.data);
scope.addExport(value.data[0], value.data[1]);
break;
case 'query':
@ -175,34 +181,62 @@ class Collector {
}
* _query(scope, name) {
let result;
let result = scope.query(name);
// Wait any information about the reference.
while ((result = scope.query(name)).type === 'unknown') {
yield;
// TODO: warning.
assert.notEqual(result.type, 'unknown');
// Resulting scope is always the best choice for waiting.
scope = result.scope;
switch (result.type) {
case 'external':
const modulePath = scope.resolve(result.info.path);
this.collect(modulePath, true);
const module = this.modules.get(modulePath);
const {imported} = result.info;
while ((result = module.query(imported)).type === 'unknown') {
yield;
}
if (result.type === 'definition') {
return result.schema;
}
// TODO: reexports.
assert.equal(result.type, 'declaration');
scope = result.scope;
name = result.name;
// Fallthrough.
case 'declaration':
this._freestyle(extractors.definition, result.node, scope);
while ((result = scope.query(name)).type === 'declaration') {
yield;
}
assert.equal(result.type, 'definition');
// Fallthrough.
case 'definition':
return result.schema;
}
}
if (result.type === 'local') {
return result.schema;
* _grabExports(module) {
for (const [scope, name] of module.exports.values()) {
yield* this._query(scope, name);
}
}
assert.equal(result.type, 'external');
// TODO: reexports.
const modulePath = scope.resolve(result.info.path);
this.collect(modulePath);
const module = this.modules.get(modulePath);
const {imported} = result.info;
let schema;
while (!(schema = module.query(imported))) {
yield;
}
return schema;
_spawn(task) {
this.tasks.add(task);
++this.taskCount;
}
_schedule() {
@ -234,7 +268,7 @@ class Collector {
}
function isNode(it) {
return it && typeof it === 'object' && (it.type || it.length);
return it && typeof it === 'object' && it.type;
}
function pathToNamespace(path) {
@ -249,8 +283,8 @@ function pathToNamespace(path) {
.join('.');
}
function getExtractorsGroup(type) {
return extractors.find(group => group.entries.includes(type));
function isAcceptableGroup(group, node) {
return group.entries.includes(node.type);
}
module.exports = Collector;

View File

@ -1,20 +1,20 @@
'use strict';
class Command {
static spawn(node) {
return new Command('spawn', node);
static declare(name, node) {
return new Command('declare', [name, node]);
}
static define(schema) {
return new Command('define', schema);
static define(schema, declared = true) {
return new Command('define', [schema, declared]);
}
static external(external) {
return new Command('external', external);
}
static provide(internal) {
return new Command('provide', internal);
static provide(name, reference) {
return new Command('provide', [name, reference]);
}
static query(name) {

View File

@ -2,11 +2,15 @@
const assert = require('assert');
const {spawn, define, external, provide, query, enter, exit, namespace} = require('./commands');
const {declare, define, external, provide, query, enter, exit, namespace} = require('./commands');
const {partition} = require('./utils');
const typedefsGroup = {
entries: ['TypeAlias', 'InterfaceDeclaration', 'ClassDeclaration'],
const definition = {
entries: [
'TypeAlias',
'InterfaceDeclaration',
'ClassDeclaration',
],
* TypeAlias(node) {
let schema = yield node.right;
@ -83,9 +87,6 @@ const typedefsGroup = {
},
* ClassMethod(node) {
// For cases, when the body has references to the class.
yield spawn(node.body);
return null;
},
@ -235,25 +236,39 @@ const typedefsGroup = {
},
};
/*
* TODO: declarations.
*/
const blocksGroup = {
entries: ['BlockStatement'],
const declaration = {
entries: [
// Blocks.
'Program',
'BlockStatement',
// Imports.
'ImportDeclaration',
'VariableDeclarator',
// Exports.
'ExportNamedDeclaration',
'ExportDefaultDeclaration',
],
/*
* Blocks.
*/
* Program(node) {
yield node.body;
},
* BlockStatement(node) {
yield enter();
yield spawn(node.body);
yield node.body;
yield exit();
},
};
/*
* TODO: warning about "import typeof".
* TODO: support form "import *".
*/
const importsGroup = {
entries: ['ImportDeclaration', 'VariableDeclarator'],
/*
* Imports.
*
* TODO: warning about "import typeof".
* TODO: support form "import *".
*/
* ImportDeclaration(node) {
const specifiers = yield node.specifiers;
@ -325,6 +340,81 @@ const importsGroup = {
};
},
/*
* Exports.
*
* TODO: support "export from" form.
* TODO: support commonjs.
*/
* ExportDefaultDeclaration(node) {
const reference = yield node.declaration;
if (reference) {
yield provide(null, reference);
}
},
* ExportNamedDeclaration(node) {
if (!node.declaration) {
yield node.specifiers;
return;
}
const reference = yield node.declaration;
if (reference) {
yield provide(reference, reference);
}
},
* ExportSpecifier(node) {
const reference = yield node.local;
let name = yield node.exported;
if (name === 'default') {
name = null;
}
yield provide(name, reference);
},
/*
* Declarations.
*/
* TypeAlias(node) {
const name = yield node.id;
yield declare(name, node);
return name;
},
* InterfaceDeclaration(node) {
const name = yield node.id;
yield declare(name, node);
return name;
},
* ClassDeclaration(node) {
const name = yield node.id;
// TODO: do it only for "all"-mode.
const body = node.body;
yield body.body.filter(is('ClassMethod'));
yield declare(name, node);
return name;
},
/*
* Utility.
*/
* StringLiteral(node) {
return node.value;
},
@ -334,64 +424,6 @@ const importsGroup = {
},
};
/*
* TODO: support "export from" form.
* TODO: support commonjs.
*/
const exportsGroup = Object.assign({}, typedefsGroup, {
entries: ['ExportNamedDeclaration', 'ExportDefaultDeclaration'],
* ExportDefaultDeclaration(node) {
let schema = yield node.declaration;
if (!schema) {
return;
}
if (typeof schema === 'string') {
schema = yield query(schema);
}
yield provide({
schema,
exported: null,
});
},
* ExportNamedDeclaration(node) {
if (!node.declaration) {
yield node.specifiers;
return;
}
const schema = yield node.declaration;
if (!schema) {
return;
}
yield provide({
schema,
exported: schema.name,
});
},
* ExportSpecifier(node) {
let exported = yield node.exported;
if (exported === 'default') {
exported = null;
}
const name = yield node.local;
const schema = yield query(name);
yield provide({
schema,
exported,
});
},
});
function* extractLastPragma(comments) {
const pragmas = (yield comments).filter(Boolean);
@ -415,7 +447,7 @@ function* extractProperty(prop, value) {
if (type.type === 'record') {
type.namespace = yield namespace();
yield define(type);
yield define(type, false);
type = type.name;
}
@ -556,9 +588,11 @@ function mergeSchemas(schemas) {
};
}
module.exports = [
typedefsGroup,
blocksGroup,
importsGroup,
exportsGroup,
];
function is(type) {
return node => Boolean(node) && node.type === type;
}
module.exports = {
definition,
declaration,
};

View File

@ -10,7 +10,7 @@ class Scope {
const global = new Scope(null, null);
for (const schema of schemas) {
global.addSchema(schema);
global.addDefinition(schema, false);
}
return global;
@ -20,8 +20,7 @@ class Scope {
this.parent = parent;
this.module = module;
this.scopeId = module && module.generateScopeId();
this.schemas = new Map;
this.imports = new Map;
this.entries = new Map;
}
get namespace() {
@ -41,22 +40,45 @@ class Scope {
return new Scope(this, module || this.module);
}
addSchema(schema) {
assert(!this.schemas.has(schema.name));
addDeclaration(name, node) {
assert(!this.entries.has(name));
this.schemas.set(schema.name, schema);
this.entries.set(name, {
type: 'declaration',
name,
node,
scope: this,
});
}
addDefinition(schema, declared = true) {
if (declared) {
assert.equal((this.entries.get(schema.name) || {}).type, 'declaration');
} else {
assert(!this.entries.has(schema.name));
}
this.entries.set(schema.name, {
type: 'definition',
schema,
scope: this,
});
}
addImport(info) {
assert(!this.imports.has(info.local));
assert(!this.entries.has(info.local));
this.imports.set(info.local, info);
this.entries.set(info.local, {
type: 'external',
info,
scope: this,
});
}
addExport(info) {
addExport(name, reference) {
assert(this.module);
this.module.addExport(info.exported, info.schema);
this.module.addExport(name, this, reference);
}
resolve(path) {
@ -66,7 +88,7 @@ class Scope {
}
query(name) {
const result = this._querySchema(name) || this._queryImport(name);
const result = this.entries.get(name);
if (result) {
return result;
@ -80,24 +102,6 @@ class Scope {
type: 'unknown',
};
}
_querySchema(name) {
const schema = this.schemas.get(name);
return schema && {
type: 'local',
schema,
};
}
_queryImport(name) {
const info = this.imports.get(name);
return info && {
type: 'external',
info,
};
}
}
class Module {
@ -112,12 +116,22 @@ class Module {
return this.scopeCount++;
}
addExport(name, schema) {
this.exports.set(name, schema);
addExport(name, scope, reference) {
this.exports.set(name, [scope, reference]);
}
query(name) {
return this.exports.get(name) || null;
const result = this.exports.get(name);
if (!result) {
return {
type: 'unknown',
};
}
const [scope, reference] = result;
return scope.query(reference);
}
resolve(path) {

View File

@ -9,3 +9,5 @@ interface Interface {
class Class {
a: number[];
}
export {Type, Interface, Class};

View File

@ -28,5 +28,5 @@
}]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -7,3 +7,5 @@ type Y = {
};
type Z = string;
export {X};

View File

@ -18,5 +18,5 @@
"fields": [{"name": "y", "type": "Y"}]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -9,3 +9,5 @@ interface Interface {
class Class {
a: 'one' | 'two';
}
export {Type, Interface, Class};

View File

@ -28,5 +28,5 @@
}]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -21,3 +21,5 @@ type Y = {
k: T,
p: P,
};
export {X, Y};

View File

@ -71,5 +71,5 @@
]
}
],
"taskCount": 18
"taskCount": 17
}

View File

@ -1,8 +1,8 @@
export type N = {
type N = {
n: boolean,
};
class M {
export class M {
m: string;
}
@ -10,8 +10,8 @@ interface KK {
k: number;
}
class P {
export default class P {
p: number;
}
export {M, KK as K, P as default};
export {N, KK as K};

View File

@ -21,3 +21,5 @@ interface Y extends X {
interface Z extends Y {
z: boolean;
}
export {C, Z};

View File

@ -51,5 +51,5 @@
]
}
],
"taskCount": 6
"taskCount": 8
}

View File

@ -11,3 +11,5 @@ type Y = {
class Z {
z: A & C;
}
export {X, Y, Z};

View File

@ -12,12 +12,6 @@
"namespace": "intersections",
"fields": [{"name": "b", "type": "string"}]
},
{
"type": "record",
"name": "C",
"namespace": "intersections",
"fields": [{"name": "c", "type": "boolean"}]
},
{
"type": "record",
"name": "X",
@ -27,6 +21,12 @@
{"name": "b", "type": "string"}
]
},
{
"type": "record",
"name": "C",
"namespace": "intersections",
"fields": [{"name": "c", "type": "boolean"}]
},
{
"type": "record",
"name": "_A_B_C",
@ -63,5 +63,5 @@
]
}
],
"taskCount": 6
"taskCount": 8
}

View File

@ -5,3 +5,5 @@ type Type = {
interface Interface {
[string]: number;
}
export {Type, Interface};

View File

@ -13,5 +13,5 @@
"values": "double"
}
],
"taskCount": 2
"taskCount": 4
}

View File

@ -36,3 +36,5 @@ class Class {
/* $avro fixed 10 */
e: Buffer;
}
export {Type, Interface, Class};

View File

@ -37,5 +37,5 @@
]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -21,3 +21,5 @@ class Class {
d: null;
e: Buffer;
}
export {Type, Interface, Class};

View File

@ -37,5 +37,5 @@
]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -17,3 +17,5 @@ class Class {
b: A[];
c: A<number>;
}
export {Type, Interface, Class};

View File

@ -36,5 +36,5 @@
]
}
],
"taskCount": 4
"taskCount": 6
}

View File

@ -20,6 +20,9 @@ type Y = {
x: X,
z: Z,
};
// TODO: replace with commonjs.
export {Y as Y2};
}
class Test {
@ -30,6 +33,8 @@ type Y = {
x: X,
z: Z,
};
export {Y as Y3};
}
baz() {
@ -39,6 +44,12 @@ type Y = {
x: X,
z: Z,
};
export {Y as Y4};
}
}
export {Y as Y1};
})();
export {Y as Y0};

View File

@ -22,22 +22,16 @@
"namespace": "scopes._1",
"fields": [{"name": "x", "type": "X"}]
},
{
"type": "string",
"name": "Z",
"namespace": "scopes._1"
},
{
"type": "record",
"name": "Test",
"namespace": "scopes._1",
"fields": []
},
{
"type": "boolean",
"name": "X",
"namespace": "scopes._2"
},
{
"type": "string",
"name": "Z",
"namespace": "scopes._1"
},
{
"type": "record",
"name": "Y",
@ -76,5 +70,5 @@
]
}
],
"taskCount": 16
"taskCount": 17
}

View File

@ -8,4 +8,9 @@ type X = {
type Y = {
y: Buffer,
};
// TODO: replace with commonjs.
export {Y};
})();
export {X};

View File

@ -23,5 +23,5 @@
]
}
],
"taskCount": 4
"taskCount": 6
}

View File

@ -29,3 +29,5 @@ class Class {
baz: () => void;
}
export {Type, Interface, Class};

View File

@ -28,5 +28,5 @@
]
}
],
"taskCount": 6
"taskCount": 8
}

View File

@ -3,5 +3,10 @@ class Test {
type X = {
t: Test,
};
// TODO: replace with commonjs.
export {X};
}
}
export {Test};

View File

@ -13,5 +13,5 @@
"fields": [{"name": "t", "type": "typeInMethod.Test"}]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -12,3 +12,5 @@ class Class {
a: string | number;
b: ?string;
}
export {Type, Interface, Class};

View File

@ -46,5 +46,5 @@
]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -9,3 +9,5 @@ interface Interface {
class Class {
a: 'one' | 'two' | number;
}
export {Type, Interface, Class};

View File

@ -37,5 +37,5 @@
}]
}
],
"taskCount": 3
"taskCount": 5
}

View File

@ -9,3 +9,5 @@ interface Interface {
class Class {
a: 'one';
}
export {Type, Interface, Class};

View File

@ -25,5 +25,5 @@
]
}
],
"taskCount": 3
"taskCount": 5
}