Support for multi-file projects

master
Paul Loyd 2017-11-04 22:13:08 +03:00
parent e2abcc4f8a
commit 345b381979
11 changed files with 504 additions and 90 deletions

View File

@ -37,7 +37,7 @@ Output:
## TODO
* External types (`import` and `require`).
* Generics.
* Errors and warnings.
* Support commonjs modules.
* Documentation.

View File

@ -6,28 +6,53 @@ const pathlib = require('path');
const extractors = require('./extractors');
const Command = require('./commands');
const Scope = require('./scope');
const {Scope, Module} = require('./scope');
const {consume, log} = require('./utils');
class Collector {
constructor(parser) {
constructor(parser, root = '.') {
this.root = root;
this.parser = parser;
this.schemas = [];
this.waiters = Object.create(null);
this.tasks = [];
this.modules = new Map;
this.global = Scope.global([]);
this.running = false;
}
collect(path) {
// TODO: follow symlinks.
path = pathlib.resolve(path);
let module = this.modules.get(path);
if (module) {
return;
}
// TODO: error wrapping.
const code = fs.readFileSync(path, 'utf8');
const ast = this.parser.parse(code);
const global = Scope.global([]);
// TODO: customize it.
const namespace = pathToNamespace(path);
const namespace = pathToNamespace(pathlib.relative('.', path));
const scope = global.extend(namespace);
module = new Module(path, namespace);
const scope = this.global.extend(module);
this._spawn(ast.program, scope);
this.modules.set(path, module);
if (!this.running) {
try {
this.running = true;
this._schedule();
} finally {
this.running = false;
}
}
}
// Given the AST output of babylon parse, walk through in a depth-first order.
@ -57,7 +82,7 @@ class Collector {
if (type && type in extractors) {
const task = this._collect(node, scope);
this._schedule(task);
this.tasks.push(task);
continue;
}
@ -97,24 +122,21 @@ class Collector {
this._spawn(value.data, scope);
break;
case 'provide':
const schema = value.data;
case 'define':
scope.addSchema(value.data);
this.schemas.push(value.data);
scope.addSchema(schema);
this.schemas.push(schema);
this._wakeup(schema.name);
break;
case 'external':
scope.addImport(value.data);
break;
case 'provide':
scope.addExport(value.data);
break;
case 'query':
let info;
while (!(info = scope.query(value.data))) {
yield value.data;
}
// TODO: support externals.
assert(info.type, 'local');
result = info.schema;
result = yield* this._query(scope, value.data);
break;
case 'enter':
@ -146,28 +168,50 @@ class Collector {
}
}
_schedule(task) {
const {done, value} = task.next();
* _query(scope, name) {
let result;
if (done) {
return;
// Wait any information about the reference.
while ((result = scope.query(name)).type === 'unknown') {
yield;
}
const waiters = this.waiters[value] = this.waiters[value] || [];
if (result.type === 'local') {
return result.schema;
}
waiters.push(task);
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;
}
_wakeup(name) {
if (!this.waiters[name]) {
return;
}
_schedule() {
// TODO: prevent infinite loop.
const tasks = this.waiters[name];
delete this.waiters[name];
const {tasks} = this;
for (const task of tasks) {
this._schedule(task);
for (let i = 0; tasks.length > 0; i = ++i % tasks.length) {
const {done} = tasks[i].next();
if (done) {
// TODO: use linked list instead.
tasks.splice(i, 1);
--i;
}
}
}
}

View File

@ -5,8 +5,16 @@ class Command {
return new Command('spawn', node);
}
static provide(schema) {
return new Command('provide', schema);
static define(schema) {
return new Command('define', schema);
}
static external(external) {
return new Command('external', external);
}
static provide(internal) {
return new Command('provide', internal);
}
static query(name) {

View File

@ -2,7 +2,7 @@
const assert = require('assert');
const {spawn, provide, query, enter, exit, namespace} = require('./commands');
const {spawn, define, external, provide, query, enter, exit, namespace} = require('./commands');
const {partition} = require('./utils');
const extractors = {
@ -16,7 +16,9 @@ const extractors = {
schema.name = yield node.id;
schema.namespace = yield namespace();
yield provide(schema);
yield define(schema);
return schema;
},
* InterfaceDeclaration(node) {
@ -40,7 +42,9 @@ const extractors = {
schema.name = yield node.id;
schema.namespace = yield namespace();
yield provide(schema);
yield define(schema);
return schema;
},
* ClassDeclaration(node) {
@ -56,7 +60,9 @@ const extractors = {
schema.name = yield node.id;
schema.namespace = yield namespace();
yield provide(schema);
yield define(schema);
return schema;
},
* ClassBody(node) {
@ -75,6 +81,7 @@ const extractors = {
},
* ClassMethod(node) {
// For cases, when the body has references to the class.
yield spawn(node.body);
return null;
@ -230,6 +237,144 @@ const extractors = {
* CommentBlock(node) {
return extractPragma(node.value);
},
/*
* Imports.
*
* TODO: warning about "import typeof".
* TODO: support form "import *".
*/
* ImportDeclaration(node) {
const specifiers = yield node.specifiers;
const path = yield node.source;
for (const specifier of specifiers) {
specifier.path = path;
yield external(specifier);
}
},
* ImportDefaultSpecifier(node) {
return {
local: yield node.local,
imported: null,
};
},
* ImportSpecifier(node) {
return {
local: yield node.local,
imported: yield node.imported,
};
},
* VariableDeclarator(node) {
const path = extractRequire(node.init);
if (!path) {
return null;
}
let specifiers = yield node.id;
if (typeof specifiers === 'string') {
specifiers = [{
local: specifiers,
imported: null,
}];
}
for (const specifier of specifiers) {
specifier.path = path;
yield external(specifier);
}
},
* ObjectPattern(node) {
return yield node.properties;
},
* ObjectProperty(node) {
const key = yield node.key;
// TODO: different roots.
if (node.value.type !== 'Identifier') {
return null;
}
//assert.equal(node.value.type, 'Identifier');
const value = yield node.value;
return {
local: value,
imported: key,
};
},
* StringLiteral(node) {
return node.value;
},
/*
* Exports.
*
* TODO: support "export from" form.
* TODO: support commonjs.
*/
* 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) {
@ -255,7 +400,7 @@ function* extractProperty(prop, value) {
if (type.type === 'record') {
type.namespace = yield namespace();
yield provide(type);
yield define(type);
type = type.name;
}
@ -265,6 +410,26 @@ function* extractProperty(prop, value) {
};
}
function extractRequire(node) {
// XXX: refactor it!
const ok = node &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require';
if (!ok) {
return null;
}
const argument = node.arguments[0];
// TODO: warning about dynamic imports.
assert.equal(argument.type, 'StringLiteral');
return argument.value;
}
function parsePragma(pragma) {
let [type, arg] = pragma.split(/\s+/);

View File

@ -1,26 +1,44 @@
'use strict';
const assert = require('assert');
const pathlib = require('path');
const resolve = require('resolve');
class Scope {
static global(schemas) {
return new Scope(null, '', schemas);
}
const global = new Scope(null, null);
constructor(parent, namespace, schemas) {
this.parent = parent;
this.schemas = new Map(schemas);
this.externals = new Map;
this.namespace = namespace;
this.childCount = 0;
}
extend(namespace = null) {
if (!namespace) {
namespace = `${this.namespace}._${this.childCount++}`;
for (const schema of schemas) {
global.addSchema(schema);
}
return new Scope(this, namespace, []);
return global;
}
constructor(parent, module) {
this.parent = parent;
this.module = module;
this.scopeId = module && module.generateScopeId();
this.schemas = new Map;
this.imports = new Map;
}
get namespace() {
assert(this.module);
let namespace = this.module.namespace;
// Nested scopes.
if (this.scopeId > 0) {
namespace += '._' + this.scopeId;
}
return namespace;
}
extend(module = null) {
return new Scope(this, module || this.module);
}
addSchema(schema) {
@ -29,12 +47,26 @@ class Scope {
this.schemas.set(schema.name, schema);
}
addExternal(external) {
this.externals.set(external.name, external);
addImport(info) {
assert(!this.imports.has(info.local));
this.imports.set(info.local, info);
}
addExport(info) {
assert(this.module);
this.module.addExport(info.exported, info.schema);
}
resolve(path) {
assert(this.module);
return this.module.resolve(path);
}
query(name) {
const result = this._querySchema(name) || this._queryExternal(name);
const result = this._querySchema(name) || this._queryImport(name);
if (result) {
return result;
@ -44,26 +76,59 @@ class Scope {
return this.parent.query(name);
}
return null;
return {
type: 'unknown',
};
}
_querySchema(name) {
const schema = this.schemas.get(name);
return schema ? {
return schema && {
type: 'local',
schema,
} : null;
};
}
_queryExternal(name) {
const info = this.externals.get(name);
_queryImport(name) {
const info = this.imports.get(name);
return info ? {
return info && {
type: 'external',
info,
} : null;
};
}
}
module.exports = Scope;
class Module {
constructor(path, namespace) {
this.path = path;
this.namespace = namespace;
this.scopeCount = 0;
this.exports = new Map;
}
generateScopeId() {
return this.scopeCount++;
}
addExport(name, schema) {
this.exports.set(name, schema);
}
query(name) {
return this.exports.get(name) || null;
}
resolve(path) {
const basedir = pathlib.dirname(this.path);
// TODO: follow symlinks.
return resolve.sync(path, {basedir});
}
}
module.exports = {
Scope,
Module,
};

View File

@ -23,10 +23,11 @@
},
"dependencies": {
"babylon": "^6.18.0",
"optimist": "^0.6.1"
"optimist": "^0.6.1",
"resolve": "^1.5.0"
},
"devDependencies": {
"jsondiffpatch": "^0.2.5"
"jsondiffpatch": "^0.2.5"
},
"scripts": {
"test": "tests/do"

97
tests/externals.js Normal file
View File

@ -0,0 +1,97 @@
import D, {A, B as F} from './externals/first';
import type {C} from './externals/first';
import UM from './unused/module';
const {N, M, K: T} = require('./externals/second');
const P = require('./externals/second');
const UR = require('./unused/module');
type X = {
a: A,
b: F,
c: C,
d: D,
};
type Y = {
n: N,
m: M,
k: T,
p: P,
};
// ###
[
{
type: 'record',
name: 'A',
namespace: 'externals.first',
fields: [{name: 'a', type: 'boolean'}],
},
{
type: 'record',
name: 'B',
namespace: 'externals.first',
fields: [{name: 'b', type: 'string'}],
},
{
type: 'record',
name: 'CC',
namespace: 'externals.first',
fields: [{name: 'c', type: 'double'}],
},
{
type: 'record',
name: 'D',
namespace: 'externals.first',
fields: [{name: 'd', type: 'double'}],
},
{
type: 'record',
name: 'N',
namespace: 'externals.second',
fields: [{name: 'n', type: 'boolean'}],
},
{
type: 'record',
name: 'M',
namespace: 'externals.second',
fields: [{name: 'm', type: 'string'}],
},
{
type: 'record',
name: 'KK',
namespace: 'externals.second',
fields: [{name: 'k', type: 'double'}],
},
{
type: 'record',
name: 'P',
namespace: 'externals.second',
fields: [{name: 'p', type: 'double'}],
},
{
type: 'record',
name: 'X',
namespace: 'externals',
fields: [
{name: 'a', type: 'externals.first.A'},
{name: 'b', type: 'externals.first.B'},
{name: 'c', type: 'externals.first.CC'},
{name: 'd', type: 'externals.first.D'},
],
},
{
type: 'record',
name: 'Y',
namespace: 'externals',
fields: [
{name: 'n', type: 'externals.second.N'},
{name: 'm', type: 'externals.second.M'},
{name: 'k', type: 'externals.second.KK'},
{name: 'p', type: 'externals.second.P'},
],
},
]

17
tests/externals/first.js vendored Normal file
View File

@ -0,0 +1,17 @@
export type A = {
a: boolean,
};
class B {
b: string;
}
interface CC {
c: number;
}
class D {
d: number;
}
export {B, CC as C, D as default};

17
tests/externals/second.js vendored Normal file
View File

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

View File

@ -59,65 +59,65 @@ type Y = {
{
type: 'double',
name: 'X',
namespace: 'scopes._0',
namespace: 'scopes._1',
},
{
type: 'record',
name: 'Y',
namespace: 'scopes._0',
namespace: 'scopes._1',
fields: [{name: 'x', type: 'X'}],
},
{
type: 'string',
name: 'Z',
namespace: 'scopes._0',
namespace: 'scopes._1',
},
{
type: 'record',
name: 'Test',
namespace: 'scopes._1',
fields: [],
},
{
type: 'boolean',
name: 'X',
namespace: 'scopes._0._0',
namespace: 'scopes._2',
},
{
type: 'record',
name: 'Y',
namespace: 'scopes._0._0',
namespace: 'scopes._2',
fields: [
{name: 'x', type: 'X'},
{name: 'z', type: 'scopes._0.Z'},
{name: 'z', type: 'scopes._1.Z'},
],
},
{
type: 'double',
name: 'X',
namespace: 'scopes._0._1',
namespace: 'scopes._3',
},
{
type: 'record',
name: 'Y',
namespace: 'scopes._0._1',
namespace: 'scopes._3',
fields: [
{name: 'x', type: 'X'},
{name: 'z', type: 'scopes._0.Z'},
{name: 'z', type: 'scopes._1.Z'},
],
},
{
type: 'string',
name: 'X',
namespace: 'scopes._0._2',
namespace: 'scopes._4',
},
{
type: 'record',
name: 'Y',
namespace: 'scopes._0._2',
namespace: 'scopes._4',
fields: [
{name: 'x', type: 'X'},
{name: 'z', type: 'scopes._0.Z'},
{name: 'z', type: 'scopes._1.Z'},
],
},
{
type: 'record',
name: 'Test',
namespace: 'scopes._0',
fields: [],
},
]

View File

@ -17,7 +17,7 @@ class Test {
{
type: 'record',
name: 'X',
namespace: 'typeInMethod._0',
namespace: 'typeInMethod._1',
fields: [{name: 't', type: 'typeInMethod.Test'}],
},
]